feat(api): add cached container cpu/mem fields on containers-dashboard

This commit is contained in:
kodi
2026-02-25 14:42:03 +01:00
parent 658e41cfba
commit 8e4e0067ff
2 changed files with 51 additions and 46 deletions
+1
View File
@@ -70,6 +70,7 @@ async def _stats_poller_loop():
key = _norm_container_name(st.get("Name")) key = _norm_container_name(st.get("Name"))
if not key: if not key:
continue continue
# CPUPerc returned by Podman is already percentage (0.10 == 0.10%)
cpu_val = st.get("CPUPerc") cpu_val = st.get("CPUPerc")
if cpu_val is None: if cpu_val is None:
cpu_val = st.get("CPU") cpu_val = st.get("CPU")
+49 -45
View File
@@ -501,9 +501,9 @@
if (tab === "images") { if (tab === "images") {
loadImages(); loadImages();
} }
// Start/stop live stats alleen in Containers tab // Start/stop live stats alleen in Containers tab (polling via /containers-dashboard)
if (tab === 'containers') startContainersStatsStream(); if (tab === 'containers') startContainersDashboardStatsPoll();
else stopContainersStatsStream(); else stopContainersDashboardStatsPoll();
refreshActive(); refreshActive();
} }
@@ -526,9 +526,9 @@
document.addEventListener("visibilitychange", () => { document.addEventListener("visibilitychange", () => {
if (document.visibilityState !== "visible") { if (document.visibilityState !== "visible") {
stopContainersStatsStream(); stopContainersDashboardStatsPoll();
} else if (currentTab === "containers") { } else if (currentTab === "containers") {
startContainersStatsStream(); startContainersDashboardStatsPoll();
} }
}); });
@@ -814,58 +814,69 @@
renderContainersGrouped(list, tbody, podStatus); renderContainersGrouped(list, tbody, podStatus);
} }
let containersStatsES = null; let containersDashboardStatsTimer = null;
let containersDashboardStatsInFlight = false;
function startContainersDashboardStatsPoll() {
if (containersDashboardStatsTimer) return;
// immediate tick, daarna elke 1s
pollContainersDashboardStatsOnce();
containersDashboardStatsTimer = setInterval(pollContainersDashboardStatsOnce, 1000);
}
function startContainersStatsStream() { function stopContainersDashboardStatsPoll() {
if (containersStatsES) return; if (!containersDashboardStatsTimer) return;
clearInterval(containersDashboardStatsTimer);
containersDashboardStatsTimer = null;
resetPodTotals();
}
containersStatsES = new EventSource("/api/containers/stats/stream?interval=1"); async function pollContainersDashboardStatsOnce() {
if (containersDashboardStatsInFlight) return;
containersDashboardStatsInFlight = true;
try {
const containers = await api('/containers-dashboard', 'GET');
const list = Array.isArray(containers) ? containers : (containers?.containers || []);
containersStatsES.addEventListener("stats", (ev) => { // totals per pod voor deze poll tick
let payload;
try {
payload = JSON.parse(ev.data);
} catch (e) {
// Slechte SSE chunk -> negeren zodat de stream niet "stilvalt"
return;
}
const statsList = payload?.data?.Stats || [];
// totals per pod voor deze SSE tick
const podCpu = new Map(); // podName -> cpuPct sum const podCpu = new Map(); // podName -> cpuPct sum
const podMem = new Map(); // podName -> memBytes sum const podMem = new Map(); // podName -> memBytes sum
const podMemPct = new Map(); // podName -> memPct sum const podMemPct = new Map(); // podName -> memPct sum
for (const st of statsList) { for (const c of (list || [])) {
const cname = normalizeContainerName(st?.Name); const cname = normalizeContainerName((c?.Names && c.Names[0]) ? c.Names[0] : (c?.Names || c?.Name || c?.name || ''));
if (!cname) continue; if (!cname) continue;
const key = cssSafeId(cname); const key = cssSafeId(cname);
const cpuPct = Number(st?.CPUPerc ?? st?.CPU ?? st?.AvgCPU ?? 0); const cpuRaw = c?._dashboard_cpu;
const memBytes = Number(st?.MemUsage ?? 0); const memBytesRaw = c?._dashboard_mem_usage;
const memPct = Number(st?.MemPerc ?? 0); const memPctRaw = c?._dashboard_mem_perc;
const cpuPct = Number(cpuRaw);
const memBytes = Number(memBytesRaw);
const memPct = Number(memPctRaw);
const pod = containersC2P.get(cname); const pod = containersC2P.get(cname);
if (pod) { if (pod) {
podCpu.set(pod, (podCpu.get(pod) || 0) + cpuPct); if (Number.isFinite(cpuPct)) podCpu.set(pod, (podCpu.get(pod) || 0) + cpuPct);
podMem.set(pod, (podMem.get(pod) || 0) + memBytes); if (Number.isFinite(memBytes)) podMem.set(pod, (podMem.get(pod) || 0) + memBytes);
podMemPct.set(pod, (podMemPct.get(pod) || 0) + memPct); if (Number.isFinite(memPct)) podMemPct.set(pod, (podMemPct.get(pod) || 0) + memPct);
} }
// CPU: jouw API geeft CPU als fractie (0.00384 -> 0.384%)
const cpuEl = document.getElementById(`cpu-${key}`); const cpuEl = document.getElementById(`cpu-${key}`);
if (cpuEl) cpuEl.textContent = cpuPct.toFixed(2) + "%"; if (cpuEl) cpuEl.textContent = Number.isFinite(cpuPct) ? (cpuPct.toFixed(2) + "%") : "-";
// MEM: bytes + percentage
const memEl = document.getElementById(`mem-${key}`); const memEl = document.getElementById(`mem-${key}`);
if (memEl) { if (memEl) {
const mem = formatBytes(memBytes); if (Number.isFinite(memBytes) && Number.isFinite(memPct)) {
memEl.textContent = `${mem} (${memPct.toFixed(1)}%)`; memEl.textContent = `${formatBytes(memBytes)} (${memPct.toFixed(1)}%)`;
} else {
memEl.textContent = "-";
}
} }
} }
// NA de container-loop: pod totals renderen
for (const [pod, cpuSum] of podCpu.entries()) { for (const [pod, cpuSum] of podCpu.entries()) {
const el = document.getElementById(`podcpu-${cssSafeId(pod)}`); const el = document.getElementById(`podcpu-${cssSafeId(pod)}`);
if (el) { if (el) {
@@ -878,22 +889,15 @@
const el = document.getElementById(`podmem-${cssSafeId(pod)}`); const el = document.getElementById(`podmem-${cssSafeId(pod)}`);
if (el) { if (el) {
const mp = podMemPct.get(pod) || 0; const mp = podMemPct.get(pod) || 0;
el.textContent = `${formatBytes(memSum)} (${mp.toFixed(1)}%)`; el.textContent = `${formatBytes(memSum)} (${Number(mp).toFixed(1)}%)`;
el.classList.remove('stale'); el.classList.remove('stale');
} }
} }
}); } catch (e) {
containersStatsES.onerror = () => {
resetPodTotals(); resetPodTotals();
}; } finally {
} containersDashboardStatsInFlight = false;
}
function stopContainersStatsStream() {
if (!containersStatsES) return;
containersStatsES.close();
containersStatsES = null;
resetPodTotals();
} }
function resetPodTotals() { function resetPodTotals() {