From 94a2f4586a5373cb5a0bc6115316128b43835d1d Mon Sep 17 00:00:00 2001 From: kodi Date: Fri, 27 Mar 2026 18:23:16 +0100 Subject: [PATCH] fix: cpu/mem container view --- control/app_containers.py | 5 +- webui/html/assets/js/tabs/networks.js | 206 ++++++++++++++++++-------- webui/html/index.html | 33 ++++- 3 files changed, 175 insertions(+), 69 deletions(-) diff --git a/control/app_containers.py b/control/app_containers.py index 6018878..8f4f8a8 100644 --- a/control/app_containers.py +++ b/control/app_containers.py @@ -29,7 +29,7 @@ _PODMAN_API_BASE = None _STATS_CACHE_BY_NAME = {} # name -> {"cpu": float|None, "mem_usage": float|None, "mem_perc": float|None} _STATS_CACHE_TS = None _STATS_POLLER_TASK = None -_STATS_SHOWN_NAMES: set = set() # namen van systemd-managed containers uit laatste dashboard call +_STATS_SHOWN_NAMES: set = set() # namen van alle dashboard-containers uit laatste dashboard call # --- EXEC SESSION CACHE (in-memory) --- _EXEC_SESSIONS = {} # session_id -> _ExecSessionState @@ -461,12 +461,11 @@ def init_containers_router( row["Status"] = (out or "").strip() dashboard.append(row) - # Bijwerken welke namen systemd-managed zijn (voor /stats filter) + # Bijwerken welke containernamen in het dashboard staan (voor /stats filter) global _STATS_SHOWN_NAMES _STATS_SHOWN_NAMES = { _norm_container_name((c.get("Names") or ["?"])[0]) for c in dashboard - if c.get("_dashboard_source") == "systemd" } - {"?", ""} return dashboard diff --git a/webui/html/assets/js/tabs/networks.js b/webui/html/assets/js/tabs/networks.js index b78d8c0..687734e 100644 --- a/webui/html/assets/js/tabs/networks.js +++ b/webui/html/assets/js/tabs/networks.js @@ -620,34 +620,27 @@ } let graphCtx = null; + let modalGraphCtx = null; + let _escBound = false; - function renderGraph(model, opts = {}) { - const host = document.getElementById('networksMapHost'); - if (!host) return; - - // leeg host (placeholder weg) - host.innerHTML = ''; + function _renderGraphInHost(hostEl, ctx, model, opts = {}) { + hostEl.innerHTML = ''; const tooltip = document.createElement('div'); tooltip.className = 'mapTooltip'; tooltip.style.display = 'none'; - host.appendChild(tooltip); + hostEl.appendChild(tooltip); - const w = Math.max(600, host.clientWidth || 600); - const h = Math.max(420, host.clientHeight || 420); - - const svg = d3.select(host).append('svg') - .attr('viewBox', `0 0 ${w} ${h}`); + const w = Math.max(600, hostEl.clientWidth || 600); + const h = Math.max(420, hostEl.clientHeight || 420); + const svg = d3.select(hostEl).append('svg').attr('viewBox', `0 0 ${w} ${h}`); const g = svg.append('g'); - // zoom/pan const zoom = d3.zoom() .scaleExtent([0.2, 2.5]) .on('zoom', (ev) => g.attr('transform', ev.transform)); - svg.call(zoom); - // links const link = g.append('g') .selectAll('line') .data(model.links) @@ -655,7 +648,6 @@ .append('line') .attr('class', d => d.type === 'shared' ? 'graphLink shared' : 'graphLink'); - // nodes const node = g.append('g') .selectAll('g') .data(model.nodes) @@ -663,49 +655,38 @@ .append('g') .attr('class', d => `graphNode ${d.type}`); - node.append('circle') - .attr('r', d => d.type === 'network' ? 14 : 9); - + node.append('circle').attr('r', d => d.type === 'network' ? 14 : 9); node.append('text') .attr('class', 'graphLabel') .attr('x', d => d.type === 'network' ? 18 : 12) .attr('y', 4) .text(d => d.label || d.key); - // drag const drag = d3.drag() .on('start', (ev, d) => { - if (!ev.active) graphCtx.sim.alphaTarget(0.2).restart(); + if (!ev.active) ctx.sim.alphaTarget(0.2).restart(); d.fx = d.x; d.fy = d.y; }) - .on('drag', (ev, d) => { - d.fx = ev.x; d.fy = ev.y; - }) + .on('drag', (ev, d) => { d.fx = ev.x; d.fy = ev.y; }) .on('end', (ev, d) => { - if (!ev.active) graphCtx.sim.alphaTarget(0); + if (!ev.active) ctx.sim.alphaTarget(0); d.fx = null; d.fy = null; }); - node.call(drag); - // hover highlight (connected) node.on('mouseenter', (ev, d) => { node.classed('graphDim', true); link.classed('graphDim', true); - d3.select(ev.currentTarget).classed('graphDim', false); - link.each(function(l) { const sid = l.source?.id || l.source; const tid = l.target?.id || l.target; - const hit = (sid === d.id || tid === d.id); - if (hit) { + if (sid === d.id || tid === d.id) { d3.select(this).classed('graphDim', false).classed('graphActive', true); } else { d3.select(this).classed('graphActive', false); } }); - const typeLabel = d.type === 'network' ? 'Netwerk' : 'Container'; const extra = d.type === 'network' ? `Driver: ${d?.meta?.driver || 'onbekend'}` @@ -713,15 +694,11 @@ tooltip.innerHTML = `${typeLabel}
${d.label || d.key}
${extra}`; tooltip.style.display = 'block'; }); - node.on('mousemove', (ev) => { - const rect = host.getBoundingClientRect(); - const x = (ev.clientX - rect.left) + 14; - const y = (ev.clientY - rect.top) + 14; - tooltip.style.left = `${x}px`; - tooltip.style.top = `${y}px`; + const rect = hostEl.getBoundingClientRect(); + tooltip.style.left = `${(ev.clientX - rect.left) + 14}px`; + tooltip.style.top = `${(ev.clientY - rect.top) + 14}px`; }); - node.on('mouseleave', () => { node.classed('graphDim', false); link.classed('graphDim', false).classed('graphActive', false); @@ -730,15 +707,14 @@ node.on('click', (ev, d) => { if (d.type === 'network') { - openNetworkDetail(d.key); + if (opts.onNetworkClick) opts.onNetworkClick(d.key); + else openNetworkDetail(d.key); return; } - - const s = document.getElementById('networksMapStatus'); - if (s) s.textContent = `Geselecteerd: ${d.type} ${d.key}`; + const statusEl = opts.statusEl || document.getElementById('networksMapStatus'); + if (statusEl) statusEl.textContent = `Geselecteerd: ${d.type} ${d.key}`; }); - // simulation const sim = d3.forceSimulation(model.nodes) .force('link', d3.forceLink(model.links).id(d => d.id).distance(80)) .force('charge', d3.forceManyBody().strength(-30)) @@ -746,23 +722,24 @@ .force('collide', d3.forceCollide().radius(d => d.type === 'network' ? 18 : 16)) .on('tick', () => { link - .attr('x1', d => d.source.x) - .attr('y1', d => d.source.y) - .attr('x2', d => d.target.x) - .attr('y2', d => d.target.y); - + .attr('x1', d => d.source.x).attr('y1', d => d.source.y) + .attr('x2', d => d.target.x).attr('y2', d => d.target.y); node.attr('transform', d => `translate(${d.x},${d.y})`); }); - // detail-mode: pin netwerk in het midden - if (opts.pinNetwork) { - const pinId = opts.pinNetwork; - const pinned = model.nodes.find(n => n.id === pinId); - if (pinned) { - pinned.fx = w / 2; - pinned.fy = h / 2; - } - } - graphCtx = { svg, g, sim, model, zoom }; + + if (opts.pinNetwork) { + const pinned = model.nodes.find(n => n.id === opts.pinNetwork); + if (pinned) { pinned.fx = w / 2; pinned.fy = h / 2; } + } + + ctx.svg = svg; ctx.g = g; ctx.sim = sim; ctx.model = model; ctx.zoom = zoom; + } + + function renderGraph(model, opts = {}) { + const host = document.getElementById('networksMapHost'); + if (!host) return; + graphCtx = {}; + _renderGraphInHost(host, graphCtx, model, opts); } function openNetworkDetail(networkName) { @@ -839,6 +816,79 @@ if (s) s.textContent = buildMapStatus('Global', model.nodes.length, model.links.length); } + // ---- Vergroot modal ---- + + function openModalNetworkDetail(networkName) { + const host = document.getElementById('networksMapModalHost'); + if (!host) return; + if (!modalGraphCtx) modalGraphCtx = {}; + + const model = buildNetworkDetailGraphModel(networkName); + _renderGraphInHost(host, modalGraphCtx, model, { + pinNetwork: `net:${networkName}`, + onNetworkClick: openModalNetworkDetail, + statusEl: document.getElementById('networksMapModalStatus'), + }); + + const s = document.getElementById('networksMapModalStatus'); + if (s) s.textContent = buildMapStatus(`Detail: ${networkName}`, model.nodes.length, model.links.length); + + const list = Array.isArray(state.list?.networks) ? state.list.networks : []; + const meta = list.find(n => (n?.name || n?.Name) === networkName) || { name: networkName }; + const usage = state.usage?.byNetwork?.[networkName]; + const containers = Array.isArray(usage?.containers) ? usage.containers : []; + const driver = String(meta?.driver || meta?.Driver || ''); + const subnets = fmtSubnets(meta).join(', '); + const listHtml = containers.length + ? `` + : `
Geen containers op dit netwerk.
`; + + const title = document.getElementById('networksMapModalDetailTitle'); + const body = document.getElementById('networksMapModalDetailBody'); + const side = document.getElementById('networksMapModalSide'); + if (title) title.textContent = networkName; + if (body) body.innerHTML = ` +
+
Driver
${esc(driver || '—')}
+
Subnets
${esc(subnets || '—')}
+
Containers
${containers.length}
+
${listHtml}`; + if (side) side.style.display = ''; + } + + function _openModalGlobalMap() { + const host = document.getElementById('networksMapModalHost'); + if (!host) return; + modalGraphCtx = {}; + const model = buildGlobalGraphModel(); + _renderGraphInHost(host, modalGraphCtx, model, { + onNetworkClick: openModalNetworkDetail, + statusEl: document.getElementById('networksMapModalStatus'), + }); + const s = document.getElementById('networksMapModalStatus'); + if (s) s.textContent = buildMapStatus('Global', model.nodes.length, model.links.length); + const side = document.getElementById('networksMapModalSide'); + if (side) side.style.display = 'none'; + } + + function openMapModal() { + const modal = document.getElementById('networksMapModal'); + if (!modal) return; + modal.style.display = 'flex'; + requestAnimationFrame(() => { _openModalGlobalMap(); }); + } + + function closeMapModal() { + const modal = document.getElementById('networksMapModal'); + if (modal) modal.style.display = 'none'; + if (modalGraphCtx && modalGraphCtx.sim) modalGraphCtx.sim.stop(); + modalGraphCtx = {}; + const host = document.getElementById('networksMapModalHost'); + if (host) host.innerHTML = ''; + const side = document.getElementById('networksMapModalSide'); + if (side) side.style.display = 'none'; + } + function renderNetworks() { const tbody = document.getElementById('networksTbody'); const rel = document.getElementById('networksRelations'); @@ -1019,6 +1069,44 @@ }); } + // Modal: expand knop + const expandBtn = document.getElementById('networksMapExpandBtn'); + if (expandBtn && !expandBtn.dataset.bound) { + expandBtn.dataset.bound = '1'; + expandBtn.addEventListener('click', openMapModal); + } + + // Modal: sluitknop + const modalCloseBtn = document.getElementById('networksMapModalClose'); + if (modalCloseBtn && !modalCloseBtn.dataset.bound) { + modalCloseBtn.dataset.bound = '1'; + modalCloseBtn.addEventListener('click', closeMapModal); + } + + // Modal: overlay klik + const mapModal = document.getElementById('networksMapModal'); + if (mapModal && !mapModal.dataset.bound) { + mapModal.dataset.bound = '1'; + mapModal.addEventListener('click', (ev) => { if (ev.target === mapModal) closeMapModal(); }); + } + + // Modal: terug naar global + const modalBackBtn = document.getElementById('networksMapModalBackBtn'); + if (modalBackBtn && !modalBackBtn.dataset.bound) { + modalBackBtn.dataset.bound = '1'; + modalBackBtn.addEventListener('click', _openModalGlobalMap); + } + + // ESC sluit modal (éénmalig binden) + if (!_escBound) { + _escBound = true; + document.addEventListener('keydown', (ev) => { + if (ev.key !== 'Escape') return; + const m = document.getElementById('networksMapModal'); + if (m && m.style.display !== 'none') closeMapModal(); + }); + } + const detailBody = document.getElementById('networksDetailBody'); if (detailBody && !detailBody.dataset.bound) { detailBody.dataset.bound = '1'; diff --git a/webui/html/index.html b/webui/html/index.html index 96a530b..74fbcc4 100644 --- a/webui/html/index.html +++ b/webui/html/index.html @@ -29,9 +29,7 @@
- - - + Laatste refresh: -
@@ -205,7 +203,8 @@ Alleen verbonden - Kaartweergave (placeholder) + Kaartweergave (placeholder) + @@ -777,9 +776,9 @@ function updateThemeToggleUi(theme) { const btn = document.getElementById('themeToggleBtn'); if (!btn) return; - const next = theme === 'dark' ? 'light' : 'dark'; - btn.textContent = `Theme: ${theme === 'dark' ? 'Dark' : 'Light'}`; - btn.title = `Schakel naar ${next === 'dark' ? 'dark' : 'light'} mode`; + const goingTo = theme === 'dark' ? 'light' : 'dark'; + btn.textContent = goingTo === 'light' ? '☀️' : '🌙'; + btn.title = goingTo === 'light' ? 'Schakel naar licht thema' : 'Schakel naar donker thema'; } function applyTheme(theme, persist = false) { @@ -832,5 +831,25 @@ + + +