fix: cpu/mem container view

This commit is contained in:
2026-03-27 18:23:16 +01:00
parent 7d2f19f81f
commit 94a2f4586a
3 changed files with 175 additions and 69 deletions
+2 -3
View File
@@ -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
+147 -59
View File
@@ -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 = `<strong>${typeLabel}</strong><br>${d.label || d.key}<br>${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
? `<ul class="mapDetailList">${containers.map(c => `<li>${esc(c.name || c.id.slice(0, 12))}</li>`).join('')}</ul>`
: `<div class="muted">Geen containers op dit netwerk.</div>`;
const title = document.getElementById('networksMapModalDetailTitle');
const body = document.getElementById('networksMapModalDetailBody');
const side = document.getElementById('networksMapModalSide');
if (title) title.textContent = networkName;
if (body) body.innerHTML = `
<div class="mapDetailGrid">
<div class="mapDetailKey">Driver</div><div>${esc(driver || '—')}</div>
<div class="mapDetailKey">Subnets</div><div>${esc(subnets || '—')}</div>
<div class="mapDetailKey">Containers</div><div>${containers.length}</div>
</div>${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';
+26 -7
View File
@@ -29,9 +29,7 @@
</div>
</div>
<div class="flex">
<button class="btn ghost" onclick="pingApi()">◉ Ping</button>
<button class="btn" onclick="refreshActive()">↻ Ververs</button>
<button class="btn ghost" id="themeToggleBtn" title="Schakel light/dark mode">◐ Theme</button>
<button class="btn ghost" id="themeToggleBtn" title="Schakel thema">🌙</button>
<span class="statusline headerMeta" id="lastRefreshHeader">Laatste refresh: -</span>
</div>
</div>
@@ -205,7 +203,8 @@
<input type="checkbox" id="networksMapConnectedOnly">
<span class="muted">Alleen verbonden</span>
</label>
<span class="muted" id="networksMapStatus" style="margin-left:auto;">Kaartweergave (placeholder)</span>
<span class="muted" id="networksMapStatus" style="margin-left:auto;">Kaartweergave (placeholder)</span>
<button class="btn small ghost" type="button" id="networksMapExpandBtn" title="Vergroot naar volledig scherm"></button>
</div>
</div>
@@ -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 @@
<script src="assets/js/tabs/networks.js"></script>
<script src="assets/js/tabs/images.js"></script>
<script src="assets/js/tabs/volumes.js"></script>
<!-- Netwerktopologie vergroot modal -->
<div id="networksMapModal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,.72); z-index:1000; align-items:center; justify-content:center;">
<div style="position:relative; width:90vw; height:90vh; background:var(--card-bg); border:1px solid var(--card-border); border-radius:16px; display:flex; flex-direction:column; overflow:hidden;">
<div style="display:flex; align-items:center; justify-content:space-between; padding:10px 16px; border-bottom:1px solid var(--card-border); flex:0 0 auto;">
<span class="muted mono" id="networksMapModalStatus">Netwerktopologie</span>
<button class="btn ghost" id="networksMapModalClose" title="Sluiten (Esc)"></button>
</div>
<div style="display:flex; flex:1; min-height:0;">
<div id="networksMapModalHost" style="flex:1; min-width:0; min-height:0; position:relative; overflow:hidden; background:var(--map-bg);"></div>
<div id="networksMapModalSide" style="display:none; width:280px; flex:0 0 280px; border-left:1px solid var(--card-border); overflow-y:auto; padding:12px;">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:10px;">
<strong id="networksMapModalDetailTitle">Netwerk</strong>
<button class="btn small ghost" id="networksMapModalBackBtn">← Terug</button>
</div>
<div id="networksMapModalDetailBody"></div>
</div>
</div>
</div>
</div>
</body>
</html>