This commit is contained in:
Tour
2025-12-08 22:08:21 +01:00
parent 7d5194d6fc
commit 7f2c46773e

View File

@@ -320,6 +320,22 @@
filter: drop-shadow(0 0 20px currentColor);
}
/* Lane bands (top-down layers) */
.lane-band {
fill: rgba(2, 6, 23, 0.35);
stroke: #334155;
stroke-width: 1;
pointer-events: none;
}
.lane-label {
fill: #94a3b8;
font-size: 12px;
font-weight: 700;
text-anchor: start;
pointer-events: none;
opacity: 0.7;
}
/* Network Zones */
.network-zone {
stroke-width: 3;
@@ -361,6 +377,77 @@
pointer-events: none;
}
/* Type Group Zones (same-color environments) */
.type-zone {
stroke-width: 2;
stroke-dasharray: 6,4;
rx: 14;
ry: 14;
pointer-events: none;
opacity: 0.65;
}
.type-zone-label {
fill: #cbd5e1;
font-size: 11px;
font-weight: 700;
text-anchor: start;
pointer-events: none;
text-shadow: 0 2px 4px rgba(0,0,0,0.8);
}
.type-core { stroke: #f87171; fill: rgba(239, 68, 68, 0.06); }
.type-infra { stroke: #34d399; fill: rgba(16, 185, 129, 0.06); }
.type-worker { stroke: #a78bfa; fill: rgba(139, 92, 246, 0.06); }
.type-iot { stroke: #fb923c; fill: rgba(249, 115, 22, 0.06); }
.type-client { stroke: #9ca3af; fill: rgba(107, 114, 128, 0.06); }
/* Host/Environment grouped boxes */
.host-group {
stroke-width: 2.5;
rx: 18;
ry: 18;
pointer-events: none;
opacity: 0.9;
}
.host-athena {
fill: rgba(96, 165, 250, 0.08);
stroke: #60a5fa;
}
.host-infra {
fill: rgba(16, 185, 129, 0.08);
stroke: #34d399;
}
.host-title {
fill: #e2e8f0;
font-size: 13px;
font-weight: 800;
text-anchor: start;
pointer-events: none;
text-shadow: 0 2px 4px rgba(0,0,0,0.8);
}
.host-subtitle {
fill: #94a3b8;
font-size: 11px;
font-weight: 600;
text-anchor: start;
pointer-events: none;
}
/* Traefik subpanel inside Athena */
.host-panel {
fill: rgba(96, 165, 250, 0.12);
stroke: #60a5fa;
stroke-dasharray: 6,3;
rx: 10;
ry: 10;
pointer-events: none;
}
.host-panel-label {
fill: #cbd5e1;
font-size: 11px;
font-weight: 700;
text-anchor: start;
pointer-events: none;
}
/* Legend */
.legend {
position: fixed;
@@ -549,6 +636,9 @@
// Create main group for graph elements
g = svg.append('g');
// Lanes (swimlanes) to emphasize top-down flow
const laneConfig = initLanes(g, width, height);
// Add network zone backgrounds (will be positioned after simulation)
const zonesGroup = g.append('g').attr('class', 'zones');
@@ -592,6 +682,57 @@
.attr('class', 'zone-sublabel')
.text('192.168.137.0/24 (USB Bridge)');
// Type group zones (same-colored environments)
const typeGroups = [
{ type: 'core', label: 'Core Services' },
{ type: 'infra', label: 'Infrastructure' },
{ type: 'worker', label: 'Workers' },
{ type: 'iot', label: 'IoT Devices' },
{ type: 'client', label: 'Clients' }
];
const typeZones = typeGroups.map(grp => ({
...grp,
rect: zonesGroup.append('rect')
.attr('class', `type-zone type-${grp.type}`)
.style('display', 'none'),
labelEl: zonesGroup.append('text')
.attr('class', 'type-zone-label')
.text(grp.label)
.style('display', 'none')
}));
// Host/environment groups (drawn behind links and nodes)
const hostGroups = g.append('g').attr('class', 'host-groups');
// Athena host box with Traefik subpanel
const athenaRect = hostGroups.append('rect')
.attr('class', 'host-group host-athena');
const athenaTitle = hostGroups.append('text')
.attr('class', 'host-title')
.text('Athena');
const athenaSubtitle = hostGroups.append('text')
.attr('class', 'host-subtitle')
.text('192.168.1.159');
const traefikPanel = hostGroups.append('rect')
.attr('class', 'host-panel');
const traefikPanelLabel = hostGroups.append('text')
.attr('class', 'host-panel-label')
.text('Traefik ingress');
// Infra DNS + AdGuard box
const infraRect = hostGroups.append('rect')
.attr('class', 'host-group host-infra');
const infraTitle = hostGroups.append('text')
.attr('class', 'host-title')
.text('Infra DNS + AdGuard');
const infraSubtitle = hostGroups.append('text')
.attr('class', 'host-subtitle')
.text('XU4 / C2 / AdGuard');
// Ensure host groups sit above lane bands but behind links/nodes
hostGroups.raise();
// Create arrow markers for directed edges
svg.append('defs').selectAll('marker')
.data(['arrow', 'arrow-dashed'])
@@ -686,18 +827,20 @@
.force('link', d3.forceLink(graphData.links)
.id(d => d.id)
.distance(d => d.distance || 150)
.strength(0.5))
.strength(0.55))
.force('charge', d3.forceManyBody()
.strength(-1000)
.distanceMax(400))
.strength(-900)
.distanceMax(450))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide()
.radius(d => (nodeTypes[d.type]?.radius || 30) + 15)
.strength(0.7))
.force('x', d3.forceX(width / 2).strength(0.05))
.force('y', d3.forceY(height / 2).strength(0.05))
.alphaDecay(0.02) // Slower decay = more settling time
.velocityDecay(0.4) // More damping = smoother movement
.radius(d => (nodeTypes[d.type]?.radius || 30) + 16)
.strength(0.8))
// Keep a slight horizontal centering to avoid drift
.force('x', d3.forceX(width / 2).strength(0.04))
// Stronger vertical force to keep nodes in their lanes
.force('laneY', d3.forceY(d => laneConfig.centerFor(d)).strength(0.18))
.alphaDecay(0.02)
.velocityDecay(0.42)
.on('tick', ticked);
function ticked() {
@@ -715,6 +858,9 @@
});
node.attr('transform', d => `translate(${d.x},${d.y})`);
// Update host group boxes on every tick
updateHostGroups();
}
function dragstarted(event, d) {
@@ -723,6 +869,22 @@
d.fy = d.y;
}
// Helper: calculate bounding box for a group of nodes
function calculateBounds(nodes, padding = 20) {
const xs = nodes.map(n => n.x);
const ys = nodes.map(n => n.y);
const minX = Math.min(...xs) - padding;
const maxX = Math.max(...xs) + padding;
const minY = Math.min(...ys) - padding;
const maxY = Math.max(...ys) + padding;
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY
};
}
// Helper: compute polygon points (e.g., hexagon) centered at 0,0
function polygonPoints(sides, radius) {
const points = [];
@@ -735,6 +897,61 @@
return points.join(' ');
}
// Draw/update host environment boxes (Athena and Infra DNS)
function updateHostGroups() {
// Athena contains Traefik + core services
const athenaNodes = graphData.nodes.filter(n =>
['Traefik','Dokku','Gitea','Auction','MI50'].includes(n.id)
);
if (athenaNodes.length > 0) {
const pad = 70;
const b = calculateBounds(athenaNodes, pad);
athenaRect
.attr('x', b.x)
.attr('y', b.y)
.attr('width', b.width)
.attr('height', b.height);
athenaTitle
.attr('x', b.x + 12)
.attr('y', b.y + 20);
athenaSubtitle
.attr('x', b.x + 12)
.attr('y', b.y + 36);
// Traefik subpanel at top of Athena
const panelPad = 10;
const panelHeight = 40;
traefikPanel
.attr('x', b.x + panelPad)
.attr('y', b.y + 46)
.attr('width', Math.max(0, b.width - panelPad * 2))
.attr('height', panelHeight);
traefikPanelLabel
.attr('x', b.x + panelPad + 10)
.attr('y', b.y + 46 + 24);
}
// Infra DNS + AdGuard grouping
const infraNodes = graphData.nodes.filter(n =>
['XU4','C2','AdGuard'].includes(n.id)
);
if (infraNodes.length > 0) {
const pad = 60;
const b = calculateBounds(infraNodes, pad);
infraRect
.attr('x', b.x)
.attr('y', b.y)
.attr('width', b.width)
.attr('height', b.height);
infraTitle
.attr('x', b.x + 12)
.attr('y', b.y + 20);
infraSubtitle
.attr('x', b.x + 12)
.attr('y', b.y + 36);
}
}
// Draw a shape for a node based on d.shape
function drawNodeShape(group, d) {
const r = (nodeTypes[d.type]?.radius || 30);
@@ -868,6 +1085,51 @@
d.fy = null;
}
// Initialize lane bands and return mapping helpers
function initLanes(group, width, height) {
const lanes = [
{ id: 'internet', label: '☁️ ISP / Internet' },
{ id: 'gateway', label: '🛜 Gateway (Router/Traefik ingress)' },
{ id: 'core', label: '⚡ Core Services (behind Traefik)' },
{ id: 'infra', label: '🔧 Infrastructure DNS (XU4/C2/HA)' },
{ id: 'clients', label: '👥 Clients & IoT' },
{ id: 'tether', label: '📱 Tether Network' }
];
const bandH = height / lanes.length;
const lanesGroup = group.append('g').attr('class', 'lanes');
lanes.forEach((lane, i) => {
lanesGroup.append('rect')
.attr('class', 'lane-band')
.attr('x', 0)
.attr('y', i * bandH)
.attr('width', width)
.attr('height', bandH);
lanesGroup.append('text')
.attr('class', 'lane-label')
.attr('x', 12)
.attr('y', i * bandH + 18)
.text(lane.label);
});
// keep lanes behind everything
lanesGroup.lower();
const centers = {};
lanes.forEach((lane, i) => { centers[lane.id] = i * bandH + bandH / 2; });
function laneOf(node) {
if (node.id === 'Internet') return 'internet';
if (node.id === 'Router' || node.id === 'Traefik') return 'gateway';
if (['Traefik', 'Dokku', 'Gitea', 'Auction', 'MI50'].includes(node.id)) return 'core';
if (['XU4', 'C2', 'HA'].includes(node.id)) return 'infra';
if (['Hermes', 'Plato'].includes(node.id)) return 'tether';
return 'clients';
}
function centerFor(node) {
return centers[laneOf(node)] || (height / 2);
}
return { centers, laneOf, centerFor };
}
// Hide loading
loadingEl.style.display = 'none';
@@ -921,9 +1183,8 @@
{ source: 'Internet', target: 'Router', label: 'DNS', distance: 200 },
{ source: 'Router', target: 'Traefik', distance: 120 },
{ source: 'Router', target: 'Dokku', distance: 120 },
{ source: 'Router', target: 'Gitea', distance: 120 },
{ source: 'Router', target: 'Auction', distance: 120 },
{ source: 'Router', target: 'MI50', distance: 120 },
// Ingress for core services flows via Traefik, not directly
// (Router direct links to core services removed)
{ source: 'Router', target: 'AdGuard', distance: 140 },
{ source: 'Router', target: 'XU4', distance: 140 },
{ source: 'Router', target: 'C2', distance: 140 },
@@ -935,20 +1196,25 @@
{ source: 'Router', target: 'IoT', distance: 160 },
{ source: 'Router', target: 'MIKE', distance: 140 },
{ source: 'Router', target: 'Lotte', distance: 160 },
// Proxy connections
// Proxy connections (Core services are behind Traefik)
{ source: 'Traefik', target: 'Gitea', distance: 80 },
{ source: 'Traefik', target: 'Dokku', distance: 80 },
{ source: 'Traefik', target: 'Auction', distance: 80 },
{ source: 'Traefik', target: 'MI50', distance: 100 },
// Tether connections
{ source: 'MIKE', target: 'Hermes', distance: 100 },
{ source: 'MIKE', target: 'Plato', distance: 100 },
// DNS filtering (dashed)
{ source: 'XU4', target: 'AdGuard', dashed: true, distance: 100 },
{ source: 'C2', target: 'AdGuard', dashed: true, distance: 100 },
{ source: 'Atlas', target: 'AdGuard', dashed: true, distance: 120 },
{ source: 'HA', target: 'AdGuard', dashed: true, distance: 120 },
{ source: 'Hermes', target: 'AdGuard', dashed: true, distance: 150 },
{ source: 'Plato', target: 'AdGuard', dashed: true, distance: 150 }
// Core uses Infra DNS (dashed back-edges to DNS servers)
{ source: 'Traefik', target: 'XU4', dashed: true, distance: 120 },
{ source: 'Traefik', target: 'C2', dashed: true, distance: 120 },
// Clients and services use DNS (representative dashed edges)
{ source: 'Atlas', target: 'XU4', dashed: true, distance: 120 },
{ source: 'HA', target: 'C2', dashed: true, distance: 120 },
{ source: 'Hermes', target: 'XU4', dashed: true, distance: 150 },
{ source: 'Plato', target: 'C2', dashed: true, distance: 150 }
];
return { nodes, links };