diff --git a/webui/html/assets/css/app.css b/webui/html/assets/css/app.css
index 7b18129..9c482db 100644
--- a/webui/html/assets/css/app.css
+++ b/webui/html/assets/css/app.css
@@ -571,4 +571,32 @@ pre{
}
.mapLegend .legendSwatch.net{ background: rgba(80,160,255,0.35); }
-.mapLegend .legendSwatch.ctr{ background: rgba(150,230,150,0.30); }
\ No newline at end of file
+.mapLegend .legendSwatch.ctr{ background: rgba(150,230,150,0.30); }
+
+/* ===== Netwerken kaart (D3) ===== */
+.mapHost svg { width: 100%; height: 100%; display:block; }
+
+.graphLink{
+ stroke: rgba(255,255,255,0.22);
+ stroke-width: 1.2;
+}
+.graphLink.shared{
+ stroke-dasharray: 6 4;
+ stroke: rgba(255,255,255,0.28);
+}
+
+.graphNode circle{
+ stroke: rgba(255,255,255,0.18);
+ stroke-width: 1;
+}
+.graphNode.network circle{ fill: rgba(80,160,255,0.35); }
+.graphNode.container circle{ fill: rgba(150,230,150,0.30); }
+
+.graphLabel{
+ fill: rgba(255,255,255,0.82);
+ font-size: 12px;
+ pointer-events: none;
+}
+
+.graphDim{ opacity: 0.18; }
+.graphActive{ opacity: 1; stroke: rgba(255,255,255,0.55); stroke-width: 2; }
\ No newline at end of file
diff --git a/webui/html/assets/js/tabs/networks.js b/webui/html/assets/js/tabs/networks.js
index b639f8b..f467492 100644
--- a/webui/html/assets/js/tabs/networks.js
+++ b/webui/html/assets/js/tabs/networks.js
@@ -76,6 +76,7 @@
const model = buildGlobalGraphModel();
const s = document.getElementById('networksMapStatus');
if (s) s.textContent = `Kaart: ${model.meta.nodes} nodes, ${model.meta.links} links`;
+ renderGraph(model);
} catch (e) {
console.error('[networks] buildGlobalGraphModel failed', e);
}
@@ -527,6 +528,120 @@
return { nodes, links, meta };
}
+ let graphCtx = null;
+
+ function renderGraph(model) {
+ const host = document.getElementById('networksMapHost');
+ if (!host) return;
+
+ // leeg host (placeholder weg)
+ host.innerHTML = '';
+
+ 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 g = svg.append('g');
+
+ // zoom/pan
+ svg.call(
+ d3.zoom()
+ .scaleExtent([0.2, 2.5])
+ .on('zoom', (ev) => g.attr('transform', ev.transform))
+ );
+
+ // links
+ const link = g.append('g')
+ .selectAll('line')
+ .data(model.links)
+ .enter()
+ .append('line')
+ .attr('class', d => d.type === 'shared' ? 'graphLink shared' : 'graphLink');
+
+ // nodes
+ const node = g.append('g')
+ .selectAll('g')
+ .data(model.nodes)
+ .enter()
+ .append('g')
+ .attr('class', d => `graphNode ${d.type}`);
+
+ node.append('circle')
+ .attr('r', d => d.type === 'network' ? 10 : 8);
+
+ node.append('text')
+ .attr('class', 'graphLabel')
+ .attr('x', 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();
+ d.fx = d.x; d.fy = d.y;
+ })
+ .on('drag', (ev, d) => {
+ d.fx = ev.x; d.fy = ev.y;
+ })
+ .on('end', (ev, d) => {
+ if (!ev.active) graphCtx.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) {
+ d3.select(this).classed('graphDim', false).classed('graphActive', true);
+ } else {
+ d3.select(this).classed('graphActive', false);
+ }
+ });
+ });
+
+ node.on('mouseleave', () => {
+ node.classed('graphDim', false);
+ link.classed('graphDim', false).classed('graphActive', false);
+ });
+
+ node.on('click', (ev, d) => {
+ const s = document.getElementById('networksMapStatus');
+ if (s) s.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(-220))
+ .force('center', d3.forceCenter(w / 2, h / 2))
+ .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);
+
+ node.attr('transform', d => `translate(${d.x},${d.y})`);
+ });
+
+ graphCtx = { svg, g, sim, model };
+ }
+
+
function renderNetworks() {
const tbody = document.getElementById('networksTbody');
const rel = document.getElementById('networksRelations');
@@ -688,6 +803,7 @@
const model = buildGlobalGraphModel();
const s = document.getElementById('networksMapStatus');
if (s) s.textContent = `Kaart: ${model.meta.nodes} nodes, ${model.meta.links} links`;
+ renderGraph(model);
}
}
diff --git a/webui/html/index.html b/webui/html/index.html
index 9420417..215f0c6 100644
--- a/webui/html/index.html
+++ b/webui/html/index.html
@@ -1208,6 +1208,7 @@
setInterval(() => { pingApi(); }, 20000);
})();
+