fgdf
This commit is contained in:
306
public/d2.html
306
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 };
|
||||
|
||||
Reference in New Issue
Block a user