- recorders.js: when isRemote=true, replace MAM_API_URL in sidecar env with http://<NODE_IP>:<PORT_MAM_API> so capture containers on worker host network can reach mam-api (fixes assets stuck in live status after recorder stop) - cluster.js: add GET /api/v1/cluster/metrics endpoint returning per-node cpu/ram/gpu utilization; update heartbeat handler to persist metrics JSONB - web-ui: add Resources panel to dashboard with live CPU/RAM/GPU bars per node, polling /api/v1/cluster/metrics every 5s
97 lines
3.6 KiB
JavaScript
97 lines
3.6 KiB
JavaScript
// screens-resources.jsx
|
|
// Live CPU/RAM/GPU gauges for Dashboard. Polls /api/v1/cluster/metrics every 5s.
|
|
// Falls back to mock data when endpoint unavailable.
|
|
|
|
const RESOURCE_MOCK={nodes:[
|
|
{hostname:"zampp1",cpu_util_pct:42,ram_used_mb:14336,ram_total_mb:32768,
|
|
gpus:[{name:"RTX 3060",util_pct:67,memory_used_mb:5120,memory_total_mb:12288}]},
|
|
{hostname:"zampp2",cpu_util_pct:18,ram_used_mb:8192,ram_total_mb:32768,
|
|
gpus:[{name:"RTX 3060",util_pct:12,memory_used_mb:1024,memory_total_mb:12288}]},
|
|
]};
|
|
|
|
function useClusterMetrics(){
|
|
const [data,setData]=React.useState(null);
|
|
const [usingMock,setUsingMock]=React.useState(false);
|
|
React.useEffect(()=>{
|
|
let cancelled=false;
|
|
const load=()=>{
|
|
window.ZAMPP_API.fetch('/cluster/metrics')
|
|
.then(d=>{
|
|
if(cancelled)return;
|
|
if(d&&Array.isArray(d.nodes)&&d.nodes.length>0){
|
|
setData(d);setUsingMock(false);
|
|
}else{setData(RESOURCE_MOCK);setUsingMock(true);}
|
|
})
|
|
.catch(()=>{if(!cancelled){setData(RESOURCE_MOCK);setUsingMock(true);}});
|
|
};
|
|
load();
|
|
const t=setInterval(load,5000);
|
|
return ()=>{cancelled=true;clearInterval(t);};
|
|
},[]);
|
|
return {data,usingMock};
|
|
}
|
|
|
|
function ResBar({pct,color}){
|
|
const p=Math.min(100,Math.max(0,Math.round(pct||0)));
|
|
const c=color||(p>85?'var(--warning)':p>60?'var(--accent)':'var(--success)');
|
|
return (
|
|
<div className="res-bar-wrap">
|
|
<div className="res-bar"><div className="res-bar-fill" style={{width:p+'%',background:c}}/></div>
|
|
<span className="res-bar-pct">{p}%</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function NodeResourceCard({node}){
|
|
const ramPct=node.ram_total_mb>0?(node.ram_used_mb/node.ram_total_mb)*100:0;
|
|
const ramUsed=(node.ram_used_mb/1024).toFixed(1);
|
|
const ramTotal=(node.ram_total_mb/1024).toFixed(0);
|
|
return (
|
|
<div className="panel res-node-card">
|
|
<div className="res-node-name"><span className="res-node-dot"/>{node.hostname}</div>
|
|
<div className="res-metric">
|
|
<div className="res-metric-label">CPU</div>
|
|
<ResBar pct={node.cpu_util_pct}/>
|
|
</div>
|
|
<div className="res-metric">
|
|
<div className="res-metric-label">RAM <span className="res-metric-sub">{ramUsed}/{ramTotal} GB</span></div>
|
|
<ResBar pct={ramPct} color={ramPct>85?'var(--warning)':'var(--text-2)'}/>
|
|
</div>
|
|
{(node.gpus||[]).map((gpu,i)=>{
|
|
const vramPct=gpu.memory_total_mb>0?(gpu.memory_used_mb/gpu.memory_total_mb)*100:0;
|
|
const vramUsed=(gpu.memory_used_mb/1024).toFixed(1);
|
|
const vramTotal=(gpu.memory_total_mb/1024).toFixed(0);
|
|
const lbl=(node.gpus||[]).length>1?'GPU '+(i+1):'GPU';
|
|
return (
|
|
<React.Fragment key={i}>
|
|
<div className="res-metric">
|
|
<div className="res-metric-label">{lbl} util</div>
|
|
<ResBar pct={gpu.util_pct}/>
|
|
</div>
|
|
<div className="res-metric">
|
|
<div className="res-metric-label">{lbl} VRAM <span className="res-metric-sub">{vramUsed}/{vramTotal} GB</span></div>
|
|
<ResBar pct={vramPct} color={vramPct>85?'var(--warning)':'var(--purple)'}/>
|
|
</div>
|
|
</React.Fragment>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ClusterResources(){
|
|
const {data,usingMock}=useClusterMetrics();
|
|
if(!data)return <div className="dash-panel-empty">Loading resource metrics...</div>;
|
|
return (
|
|
<div>
|
|
{usingMock&&(
|
|
<div className="res-mock-note">⚠ Metrics API unavailable - showing mock data</div>
|
|
)}
|
|
<div className="res-nodes-grid">
|
|
{data.nodes.map(n=><NodeResourceCard key={n.hostname} node={n}/>)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
window.ClusterResources=ClusterResources;
|