diff --git a/services/web-ui/public/screens-admin.jsx b/services/web-ui/public/screens-admin.jsx index e944ce4..dbef83f 100644 --- a/services/web-ui/public/screens-admin.jsx +++ b/services/web-ui/public/screens-admin.jsx @@ -1,8 +1,24 @@ // screens-admin.jsx — Users, Tokens, Containers, Cluster (graph), Settings -const { USERS, TOKENS_LIST, CONTAINERS, NODES } = window.ZAMPP_DATA; +function _normalizeNode(n, x, y) { + 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 || [], + x, y, + }; +} function Users() { + const { USERS } = window.ZAMPP_DATA; const [tab, setTab] = React.useState("users"); return (
@@ -15,8 +31,8 @@ function Users() {
- - + +
@@ -26,20 +42,23 @@ function Users() {
Last active
+ {USERS.length === 0 && ( +
No users found
+ )} {USERS.map(u => (
-
{u.avatar}
+
{u.initials || '??'}
-
{u.display}
+
{u.name}
@{u.username}
{u.role}
- {u.groups.map(g => {g})} + {(u.groups || []).map(g => {g})}
-
{u.last}
+
{u.lastSeen}
))} @@ -295,73 +314,133 @@ function CalcSlider({ label, value, onChange, min, max, step = 1, unit }) { } function Containers() { + const [containers, setContainers] = React.useState(null); + + function load() { + setContainers(null); + window.ZAMPP_API.fetch('/cluster/containers') + .then(data => setContainers(Array.isArray(data) ? data : (data.containers || []))) + .catch(() => setContainers([])); + } + + React.useEffect(() => { load(); }, []); + + const running = (containers || []).filter(c => c.state === 'running').length; + return (

Containers

Docker Compose services across the cluster
-
- - {CONTAINERS.filter(c => c.state === "running").length} / {CONTAINERS.length} running -
- + {containers !== null && containers.length > 0 && ( +
+ + {running} / {containers.length} running +
+ )} +
-
-
-
Container
-
Image
-
State
-
CPU
-
Memory
-
Ports
-
+ {containers === null && ( +
Loading…
+ )} + {containers !== null && containers.length === 0 && ( +
+
🐳
+
No container data available
+
Container metrics endpoint not yet wired
- {CONTAINERS.map(c => ( -
-
-
{c.name}
-
up {c.uptime}
-
-
{c.image}
-
- RUNNING - {c.healthy && healthy} -
-
-
-
-
+ )} + {containers !== null && containers.length > 0 && ( +
+
+
Container
+
Image
+
State
+
CPU
+
Memory
+
Ports
+
+
+ {containers.map(c => ( +
+
+
{c.name}
+
up {c.uptime}
+
+
{c.image}
+
+ RUNNING + {c.healthy && healthy} +
+
+
+
+
+
+ {(c.cpu || 0).toFixed(1)}%
- {c.cpu.toFixed(1)}% +
+
{c.mem} MB
+
{c.ports}
+
+ +
-
{c.mem} MB
-
{c.ports}
-
- - -
-
- ))} -
+ ))} +
+ )}
); } function Cluster() { + const rawNodes = window.ZAMPP_DATA.NODES; + const nodesArr = Array.isArray(rawNodes) ? rawNodes : (rawNodes?.nodes || []); + + const NODES = React.useMemo(() => { + if (!nodesArr.length) return []; + const primaryRaw = nodesArr.find(n => n.role === 'primary') || nodesArr[0]; + const others = nodesArr.filter(n => n !== primaryRaw); + const primary = _normalizeNode(primaryRaw, 0.5, 0.46); + const positioned = others.map((n, i) => { + const angle = others.length <= 1 + ? Math.PI / 2 + : (i / others.length) * 2 * Math.PI - Math.PI / 2; + return _normalizeNode(n, 0.5 + 0.32 * Math.cos(angle), 0.46 + 0.35 * Math.sin(angle)); + }); + return [primary, ...positioned]; + }, []); + const [hovered, setHovered] = React.useState(null); - const [selected, setSelected] = React.useState(NODES[0]); + const [selected, setSelected] = React.useState(NODES[0] || null); const W = 720, H = 460; - const primary = NODES.find(n => n.role === "primary"); + if (!NODES.length) { + return ( +
+
+

Cluster

+
+ +
+
+
No cluster nodes available
+
+
+ ); + } + + const primary = NODES.find(n => n.role === 'primary') || NODES[0]; const edges = NODES.filter(n => n.id !== primary.id).map(n => ({ from: primary, to: n, - alive: n.status === "online", + alive: n.status === 'online', })); + const sel = selected || NODES[0]; return (
@@ -378,22 +457,18 @@ function Cluster() {
Nodes
{NODES.length}
-
across 2 regions
-
Total CPU
-
320 cores
-
14% utilized
+
Avg CPU
+
{Math.round(NODES.reduce((a, n) => a + (n.cpu || 0), 0) / NODES.length)} %
GPUs
-
5
-
3 idle · 2 transcoding
+
{NODES.reduce((a, n) => a + n.gpus.length, 0)}
-
Memory
-
243 GB
-
15% utilized
+
Avg Memory
+
{Math.round(NODES.reduce((a, n) => a + (n.mem || 0), 0) / NODES.length)} GB
@@ -403,7 +478,6 @@ function Cluster() { Topology
-
@@ -440,8 +514,7 @@ function Cluster() { })} {NODES.map(n => { const cx = n.x * W, cy = n.y * H; - const isHovered = hovered === n.id; - const isSelected = selected.id === n.id; + const isSelected = sel && sel.id === n.id; const color = n.status === "online" ? "var(--success)" : "var(--text-4)"; return (
-
-
- - {selected.id} - {selected.role} -
-
- {selected.status}} /> - - - - -
-
-
- {selected.cpu}% -
- } /> - -
-
-
- {selected.mem} / {selected.memTotal} GB -
- } /> - {selected.gpus.length > 0 && ( -
-
GPUs ({selected.gpus.length})
- {selected.gpus.map((g, i) => ( -
- - {g} + {sel && ( +
+
+ + {sel.id} + {sel.role} +
+
+ {sel.status}} /> + + + + +
+
- ))} -
- )} - {selected.devices && ( -
-
Capture devices
- {selected.devices.map((d, i) => ( -
- {d} + {sel.cpu}% +
+ } /> + {sel.memTotal > 0 && ( + +
+
+
+ {sel.mem} / {sel.memTotal} GB
- ))} + } /> + )} + {sel.gpus.length > 0 && ( +
+
GPUs ({sel.gpus.length})
+ {sel.gpus.map((g, i) => ( +
+ + {g} +
+ ))} +
+ )} + {sel.devices && sel.devices.length > 0 && ( +
+
Capture devices
+ {sel.devices.map((d, i) => ( +
+ {d} +
+ ))} +
+ )} +
+ + + {sel.role !== "primary" && }
- )} -
- - - {selected.role !== "primary" && }
-
+ )}
@@ -582,7 +659,7 @@ function Settings() { icon="gpu" title="GPU / Transcoding" sub="NVIDIA NVENC acceleration for proxy generation and transcoding jobs" - tag={3 GPUs available} + tag={GPUs available} >