dragonflight/services/web-ui/public/screens-resources.jsx
Zac Gaetano 453103aee6 fix: use external MAM_API_URL for remote capture sidecars; add cluster metrics endpoint and dashboard resource graphs
- 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
2026-05-29 01:04:24 +00:00

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">&#9888; 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;