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
+ ? `