diff --git a/public/d2.html b/public/d2.html index 423bb4e..159e564 100644 --- a/public/d2.html +++ b/public/d2.html @@ -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 };