From 55ff2e717f255fba17283b4fd04469d10c13c31a Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Tue, 26 May 2026 18:06:30 +0000 Subject: [PATCH] feat(cluster): full hardware breakdown per node _normalizeNode: - Maps cpu_usage, mem_used_mb/mem_total_mb from actual API fields - Reads capabilities.gpus: name, memory_mb, device, bound status (bound = nvidia-smi confirmed driver, detected by name+memory_mb) - Reads capabilities.blackmagic + blackmagic_model: model, port count, device paths Node detail panel: - GPUs: name, VRAM, device path, BOUND/UNBOUND badge (green if driver active) - Capture cards: model name, port count badge, per-port device name with online/offline color coding Stat row: adds Capture ports total count card Topology SVG: shows GPU count and BMD port count under each node label Fix: removeNode uses node.dbId (UUID) not node.id (hostname) --- services/web-ui/public/screens-admin.jsx | 155 ++++++++++++++++++----- 1 file changed, 123 insertions(+), 32 deletions(-) diff --git a/services/web-ui/public/screens-admin.jsx b/services/web-ui/public/screens-admin.jsx index e6d66e7..0d93cbb 100644 --- a/services/web-ui/public/screens-admin.jsx +++ b/services/web-ui/public/screens-admin.jsx @@ -1,18 +1,46 @@ // screens-admin.jsx — Users, Tokens, Containers, Cluster (graph), Settings function _normalizeNode(n, x, y) { + const cap = n.capabilities || {}; + + // GPUs: capabilities.gpus entries with name+memory_mb = driver-bound (nvidia-smi confirmed). + // Entries with only type+device = detected by /dev file but driver status unknown. + const gpus = (cap.gpus || []).map(g => ({ + name: g.name || (g.type ? g.type.toUpperCase() : 'GPU'), + memMb: g.memory_mb || null, + index: g.index ?? 0, + device: g.device || null, + bound: !!(g.name && g.memory_mb), // name+memory = nvidia-smi confirmed driver bound + })); + + // Blackmagic DeckLink: capabilities.blackmagic + capabilities.blackmagic_model + const bmdPorts = (cap.blackmagic || []).map(b => ({ + index: b.index ?? 0, + device: b.device || null, + model: cap.blackmagic_model || null, + online: b.online !== false, + })); + + const memUsedMb = n.mem_used_mb || n.memory_used_mb || (n.mem && n.mem < 1000 ? n.mem * 1024 : n.mem || 0); + const memTotalMb = n.mem_total_mb || n.memory_total_mb || (n.memTotal && n.memTotal < 1000 ? n.memTotal * 1024 : n.memTotal || 0); + return { - id: n.id || n.hostname || n.name || 'node', - role: n.role || 'worker', - status: n.status || (n.online ? 'online' : 'offline'), - ip: n.ip || n.ip_address || '—', - version: n.version || '—', - uptime: n.uptime || '—', - cpu: n.cpu || n.cpu_percent || 0, - mem: n.mem || n.memory_used || n.memory_used_gb || 0, - memTotal: n.memTotal || n.mem_total || n.memory_total || n.memory_total_gb || 0, - gpus: n.gpus || (n.gpu_count ? Array(n.gpu_count).fill('GPU') : []), - devices: n.devices || n.capture_devices || [], + id: n.hostname || n.id || n.name || 'node', + dbId: n.id, + role: n.role || 'worker', + status: n.status || (n.online ? 'online' : 'offline'), + ip: n.ip_address || n.ip || '—', + version: n.version || '—', + uptime: n.uptime || '—', + cpu: parseFloat(n.cpu_usage || n.cpu || n.cpu_percent || 0), + mem: Math.round(memUsedMb / 1024 * 10) / 10, + memTotal: Math.round(memTotalMb / 1024 * 10) / 10, + // Raw capabilities for the hardware panel + gpus, + bmdPorts, + // Legacy flat arrays kept for the stat-row summary cards + gpuCount: gpus.length, + bmdCount: bmdPorts.length, x, y, }; } @@ -978,7 +1006,7 @@ function Cluster() { const removeNode = (node) => { if (!window.confirm('Remove node ' + node.id + ' from the cluster?\nThis does not stop the machine — it only removes it from cluster membership.')) return; - window.ZAMPP_API.fetch('/cluster/nodes/' + encodeURIComponent(node.id), { method: 'DELETE' }) + window.ZAMPP_API.fetch('/cluster/nodes/' + encodeURIComponent(node.dbId || node.id), { method: 'DELETE' }) .then(() => refresh()) .catch(e => setAdviceModal({ title: 'Remove failed', lines: [e.message] })); }; @@ -1011,7 +1039,11 @@ function Cluster() {
GPUs
-
{NODES.reduce((a, n) => a + n.gpus.length, 0)}
+
{NODES.reduce((a, n) => a + n.gpuCount, 0)}
+
+
+
Capture ports
+
{NODES.reduce((a, n) => a + n.bmdCount, 0)}
Avg Memory
@@ -1076,7 +1108,12 @@ function Cluster() { {n.role === "primary" && } {n.role !== "primary" && {n.role[0].toUpperCase()}} {n.id} - {n.ip} + {n.ip} + {(n.gpuCount > 0 || n.bmdCount > 0) && ( + + {[n.gpuCount > 0 && `${n.gpuCount}×GPU`, n.bmdCount > 0 && `${n.bmdCount}×BMD`].filter(Boolean).join(' ')} + + )} ); })} @@ -1113,27 +1150,81 @@ function Cluster() {
} /> )} - {sel.gpus.length > 0 && ( -
-
GPUs ({sel.gpus.length})
- {sel.gpus.map((g, i) => ( -
- - {g} -
- ))} + {/* ── GPU hardware ── */} +
+
+ + GPUs {sel.gpuCount > 0 ? `(${sel.gpuCount})` : '— none reported'}
- )} - {sel.devices && sel.devices.length > 0 && ( -
-
Capture devices
- {sel.devices.map((d, i) => ( -
- {d} + {sel.gpus.length === 0 && ( +
No GPUs detected on this node
+ )} + {sel.gpus.map((g, i) => ( +
+ +
+
{g.name}
+ {g.memMb && ( +
+ {g.memMb >= 1024 ? (g.memMb / 1024).toFixed(1) + ' GB' : g.memMb + ' MB'} VRAM +
+ )} + {g.device &&
{g.device}
}
- ))} + + {g.bound ? "BOUND" : "UNBOUND"} + +
+ ))} +
+ + {/* ── Capture cards ── */} +
+
+ + Capture cards {sel.bmdCount > 0 ? `(${sel.bmdCount} port${sel.bmdCount !== 1 ? 's' : ''})` : '— none reported'}
- )} + {sel.bmdPorts.length === 0 && ( +
No DeckLink cards detected on this node
+ )} + {sel.bmdPorts.length > 0 && ( +
+
+ + + {sel.bmdPorts[0].model || "Blackmagic DeckLink"} + + + {sel.bmdCount} PORT{sel.bmdCount !== 1 ? 'S' : ''} + +
+
+ {sel.bmdPorts.map((p, i) => ( +
+ {p.device ? p.device.split('/').pop() : `port ${p.index}`} +
+ ))} +
+
+ )} +