diff --git a/public/d2.html b/public/d2.html
index 014e5ca..423bb4e 100644
--- a/public/d2.html
+++ b/public/d2.html
@@ -269,6 +269,19 @@
filter: drop-shadow(0 4px 12px rgba(96, 165, 250, 0.6));
transform: scale(1.1);
}
+ /* Generic shape styling for all node shapes (rects, polygons, paths, etc.) */
+ .node-shape {
+ filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3));
+ transition: all 0.3s ease;
+ }
+ .node-group:hover .node-shape {
+ filter: drop-shadow(0 4px 12px rgba(96, 165, 250, 0.6));
+ transform: scale(1.08);
+ }
+ .node-group.selected .node-shape {
+ stroke-width: 4px !important;
+ filter: drop-shadow(0 0 20px currentColor);
+ }
.node-label {
pointer-events: none;
user-select: none;
@@ -309,19 +322,42 @@
/* Network Zones */
.network-zone {
- fill: rgba(30, 41, 59, 0.3);
- stroke: #475569;
- stroke-width: 2;
+ stroke-width: 3;
stroke-dasharray: 10,5;
rx: 20;
ry: 20;
pointer-events: none;
+ opacity: 0.8;
+ }
+ .zone-isp {
+ fill: rgba(14, 165, 233, 0.08);
+ stroke: #0ea5e9;
+ }
+ .zone-lan {
+ fill: rgba(16, 185, 129, 0.08);
+ stroke: #10b981;
+ }
+ .zone-apps {
+ fill: rgba(239, 68, 68, 0.08);
+ stroke: #ef4444;
+ }
+ .zone-tether {
+ fill: rgba(139, 92, 246, 0.08);
+ stroke: #8b5cf6;
}
.zone-label {
+ fill: #e2e8f0;
+ font-size: 13px;
+ font-weight: 700;
+ text-anchor: start;
+ pointer-events: none;
+ text-shadow: 0 2px 4px rgba(0,0,0,0.8);
+ }
+ .zone-sublabel {
fill: #94a3b8;
- font-size: 14px;
- font-weight: 600;
- text-anchor: middle;
+ font-size: 10px;
+ font-weight: 500;
+ text-anchor: start;
pointer-events: none;
}
@@ -516,21 +552,45 @@
// Add network zone backgrounds (will be positioned after simulation)
const zonesGroup = g.append('g').attr('class', 'zones');
+ // ISP/Internet Zone
+ const ispZone = zonesGroup.append('rect')
+ .attr('class', 'network-zone zone-isp');
+ const ispLabel = zonesGroup.append('text')
+ .attr('class', 'zone-label')
+ .text('☁️ ISP / Internet');
+ const ispSublabel = zonesGroup.append('text')
+ .attr('class', 'zone-sublabel')
+ .text('External Services');
+
+ // LAN Zone
const lanZone = zonesGroup.append('rect')
- .attr('class', 'network-zone lan-zone')
- .attr('fill', 'rgba(16, 185, 129, 0.1)');
-
- const tetherZone = zonesGroup.append('rect')
- .attr('class', 'network-zone tether-zone')
- .attr('fill', 'rgba(139, 92, 246, 0.1)');
-
+ .attr('class', 'network-zone zone-lan');
const lanLabel = zonesGroup.append('text')
.attr('class', 'zone-label')
- .text('LAN 192.168.1.0/24');
+ .text('🏠 Home LAN');
+ const lanSublabel = zonesGroup.append('text')
+ .attr('class', 'zone-sublabel')
+ .text('192.168.1.0/24');
+ // Applications Zone (inside LAN)
+ const appsZone = zonesGroup.append('rect')
+ .attr('class', 'network-zone zone-apps');
+ const appsLabel = zonesGroup.append('text')
+ .attr('class', 'zone-label')
+ .text('⚡ Application Stack');
+ const appsSublabel = zonesGroup.append('text')
+ .attr('class', 'zone-sublabel')
+ .text('Traefik + Services (192.168.1.159)');
+
+ // Tether Zone
+ const tetherZone = zonesGroup.append('rect')
+ .attr('class', 'network-zone zone-tether');
const tetherLabel = zonesGroup.append('text')
.attr('class', 'zone-label')
- .text('Tether 192.168.137.0/24');
+ .text('📱 Tether Network');
+ const tetherSublabel = zonesGroup.append('text')
+ .attr('class', 'zone-sublabel')
+ .text('192.168.137.0/24 (USB Bridge)');
// Create arrow markers for directed edges
svg.append('defs').selectAll('marker')
@@ -567,15 +627,12 @@
.on('drag', dragged)
.on('end', dragended));
- // Add circles to nodes
- node.append('circle')
- .attr('class', 'node-circle')
- .attr('r', d => nodeTypes[d.type]?.radius || 30)
- .attr('fill', d => nodeTypes[d.type]?.fill || '#6b7280')
- .attr('stroke', d => nodeTypes[d.type]?.stroke || '#9ca3af')
- .attr('stroke-width', 3);
+ // Draw meaningful shapes per node instead of circles
+ node.each(function(d) {
+ drawNodeShape(d3.select(this), d);
+ });
- // Add icon/emoji to nodes
+ // Add icon/emoji to nodes (centered)
node.append('text')
.attr('class', 'node-icon')
.attr('text-anchor', 'middle')
@@ -624,16 +681,23 @@
}
});
- // Create force simulation
+ // Create force simulation with better positioning
simulation = d3.forceSimulation(graphData.nodes)
.force('link', d3.forceLink(graphData.links)
.id(d => d.id)
- .distance(d => d.distance || 150))
+ .distance(d => d.distance || 150)
+ .strength(0.5))
.force('charge', d3.forceManyBody()
- .strength(-800))
+ .strength(-1000)
+ .distanceMax(400))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide()
- .radius(d => (nodeTypes[d.type]?.radius || 30) + 10))
+ .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
.on('tick', ticked);
function ticked() {
@@ -659,6 +723,140 @@
d.fy = d.y;
}
+ // Helper: compute polygon points (e.g., hexagon) centered at 0,0
+ function polygonPoints(sides, radius) {
+ const points = [];
+ for (let i = 0; i < sides; i++) {
+ const angle = (Math.PI * 2 * i) / sides - Math.PI / 2;
+ const x = Math.cos(angle) * radius;
+ const y = Math.sin(angle) * radius;
+ points.push(`${x},${y}`);
+ }
+ return points.join(' ');
+ }
+
+ // Draw a shape for a node based on d.shape
+ function drawNodeShape(group, d) {
+ const r = (nodeTypes[d.type]?.radius || 30);
+ const fill = nodeTypes[d.type]?.fill || '#6b7280';
+ const stroke = nodeTypes[d.type]?.stroke || '#9ca3af';
+
+ switch (d.shape) {
+ case 'cloud': {
+ // Cloud using three ellipses
+ group.append('ellipse')
+ .attr('class', 'node-shape')
+ .attr('cx', 0).attr('cy', -r * 0.2)
+ .attr('rx', r * 0.9).attr('ry', r * 0.6)
+ .attr('fill', fill).attr('stroke', stroke).attr('stroke-width', 3);
+ group.append('ellipse')
+ .attr('class', 'node-shape')
+ .attr('cx', -r * 0.7).attr('cy', -r * 0.15)
+ .attr('rx', r * 0.5).attr('ry', r * 0.35)
+ .attr('fill', fill).attr('stroke', stroke).attr('stroke-width', 3);
+ group.append('ellipse')
+ .attr('class', 'node-shape')
+ .attr('cx', r * 0.7).attr('cy', -r * 0.1)
+ .attr('rx', r * 0.5).attr('ry', r * 0.35)
+ .attr('fill', fill).attr('stroke', stroke).attr('stroke-width', 3);
+ break;
+ }
+ case 'hex': {
+ group.append('polygon')
+ .attr('class', 'node-shape')
+ .attr('points', polygonPoints(6, r))
+ .attr('fill', fill).attr('stroke', stroke).attr('stroke-width', 3);
+ break;
+ }
+ case 'router': {
+ const w = r * 2.1, h = r * 1.2;
+ group.append('rect')
+ .attr('class', 'node-shape')
+ .attr('x', -w / 2).attr('y', -h / 2)
+ .attr('width', w).attr('height', h)
+ .attr('rx', 14).attr('ry', 14)
+ .attr('fill', fill).attr('stroke', stroke).attr('stroke-width', 3);
+ // Simple antenna
+ group.append('line')
+ .attr('x1', 0).attr('y1', -h / 2)
+ .attr('x2', 0).attr('y2', -h / 2 - r * 0.6)
+ .attr('stroke', stroke).attr('stroke-width', 3);
+ break;
+ }
+ case 'server': {
+ const w = r * 2.0, h = r * 1.6;
+ group.append('rect')
+ .attr('class', 'node-shape')
+ .attr('x', -w / 2).attr('y', -h / 2)
+ .attr('width', w).attr('height', h)
+ .attr('rx', 10).attr('ry', 10)
+ .attr('fill', fill).attr('stroke', stroke).attr('stroke-width', 3);
+ // Status lights/lines
+ const y1 = -h / 4, y2 = 0, y3 = h / 4;
+ [y1, y2, y3].forEach((yy, i) => {
+ group.append('line')
+ .attr('x1', -w/2 + 10).attr('y1', yy)
+ .attr('x2', w/2 - 10).attr('y2', yy)
+ .attr('stroke', i === 0 ? '#93c5fd' : '#94a3b8')
+ .attr('stroke-width', 2).attr('opacity', 0.8);
+ });
+ break;
+ }
+ case 'chip': {
+ const w = r * 2.0, h = r * 1.6;
+ group.append('rect')
+ .attr('class', 'node-shape')
+ .attr('x', -w / 2).attr('y', -h / 2)
+ .attr('width', w).attr('height', h)
+ .attr('rx', 8).attr('ry', 8)
+ .attr('fill', fill).attr('stroke', stroke).attr('stroke-width', 3);
+ // Pins
+ const pinCount = 5, pinLen = 6;
+ for (let i = 0; i < pinCount; i++) {
+ const t = ((i + 1) / (pinCount + 1));
+ const y = -h/2 + t * h;
+ // left pins
+ group.append('line').attr('x1', -w/2).attr('y1', y).attr('x2', -w/2 - pinLen).attr('y2', y).attr('stroke', stroke).attr('stroke-width', 2);
+ // right pins
+ group.append('line').attr('x1', w/2).attr('y1', y).attr('x2', w/2 + pinLen).attr('y2', y).attr('stroke', stroke).attr('stroke-width', 2);
+ }
+ break;
+ }
+ case 'monitor': {
+ const w = r * 2.0, h = r * 1.3;
+ group.append('rect')
+ .attr('class', 'node-shape')
+ .attr('x', -w / 2).attr('y', -h / 2)
+ .attr('width', w).attr('height', h)
+ .attr('rx', 6).attr('ry', 6)
+ .attr('fill', fill).attr('stroke', stroke).attr('stroke-width', 3);
+ group.append('rect')
+ .attr('x', -w * 0.15).attr('y', h / 2 - 4)
+ .attr('width', w * 0.3).attr('height', 8)
+ .attr('fill', stroke).attr('rx', 3).attr('ry', 3);
+ break;
+ }
+ default: {
+ // Fallback circle
+ group.append('circle')
+ .attr('class', 'node-shape')
+ .attr('r', r)
+ .attr('fill', fill).attr('stroke', stroke).attr('stroke-width', 3);
+ }
+ }
+
+ // Extra overlay: red cross for AdGuard
+ if (d.id === 'AdGuard') {
+ const s = r * 0.9;
+ group.append('line')
+ .attr('x1', -s/2).attr('y1', -s/2).attr('x2', s/2).attr('y2', s/2)
+ .attr('stroke', '#ef4444').attr('stroke-width', 4).attr('opacity', 0.9);
+ group.append('line')
+ .attr('x1', s/2).attr('y1', -s/2).attr('x2', -s/2).attr('y2', s/2)
+ .attr('stroke', '#ef4444').attr('stroke-width', 4).attr('opacity', 0.9);
+ }
+ }
+
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
@@ -688,26 +886,34 @@
// Create graph data structure from network data
function createGraphData() {
const nodes = [
- { id: 'Internet', label: 'Internet', sublabel: '5.132.33.195', icon: '☁️', type: 'dns' },
- { id: 'Router', label: 'Zyxel Router', sublabel: '192.168.1.1', icon: '🛜', type: 'router' },
- { id: 'Traefik', label: 'Traefik', sublabel: '192.168.1.159', icon: '⚡', type: 'core' },
- { id: 'Dokku', label: 'Dokku', sublabel: '192.168.1.159', icon: '⚡', type: 'core' },
- { id: 'Gitea', label: 'Gitea', sublabel: 'git.appmodel.nl', icon: '⚡', type: 'core' },
- { id: 'Auction', label: 'Auction', sublabel: 'auction.appmodel.nl', icon: '⚡', type: 'core' },
- { id: 'MI50', label: 'MI50/Ollama', sublabel: 'ollama.lan', icon: '⚡', type: 'core' },
- { id: 'AdGuard', label: 'AdGuard', sublabel: 'DNS Filter', icon: '🔧', type: 'infra' },
- { id: 'XU4', label: 'XU4 DNS', sublabel: '192.168.1.163', icon: '🔧', type: 'infra' },
- { id: 'C2', label: 'C2 DNS', sublabel: '192.168.1.227', icon: '🔧', type: 'infra' },
- { id: 'HA', label: 'Home Assistant', sublabel: '192.168.1.193', icon: '🔧', type: 'infra' },
- { id: 'Atlas', label: 'Atlas', sublabel: '192.168.1.100', icon: '💻', type: 'worker' },
- { id: 'TV', label: 'Kamer-TV', sublabel: '192.168.1.240', icon: '📺', type: 'iot' },
- { id: 'Hue', label: 'Philips Hue', sublabel: '192.168.1.49', icon: '💡', type: 'iot' },
- { id: 'Eufy', label: 'Eufy S380HB', sublabel: '192.168.1.59', icon: '📹', type: 'iot' },
- { id: 'IoT', label: 'IoT Devices', sublabel: 'Various', icon: '🔌', type: 'iot' },
- { id: 'MIKE', label: 'MIKE PC', sublabel: '192.168.1.100', icon: '💻', type: 'client' },
- { id: 'Lotte', label: 'Lotte', sublabel: '192.168.1.133', icon: '📱', type: 'client' },
- { id: 'Hermes', label: 'Hermes', sublabel: '192.168.137.239', icon: '💻', type: 'worker' },
- { id: 'Plato', label: 'Plato', sublabel: 'llm.plato.lan', icon: '💻', type: 'worker' }
+ { id: 'Internet', label: 'Internet', sublabel: '5.132.33.195', icon: '☁️', type: 'dns', shape: 'cloud' },
+ { id: 'Router', label: 'Zyxel Router', sublabel: '192.168.1.1', icon: '🛜', type: 'router', shape: 'router' },
+
+ // Application stack (Traefik with nginx-like hexagon)
+ { id: 'Traefik', label: 'Traefik', sublabel: '192.168.1.159', icon: 'N', type: 'core', shape: 'hex' },
+ { id: 'Dokku', label: 'Dokku', sublabel: '192.168.1.159', icon: '⚡', type: 'core', shape: 'server' },
+ { id: 'Gitea', label: 'Gitea', sublabel: 'git.appmodel.nl', icon: '⚡', type: 'core', shape: 'server' },
+ { id: 'Auction', label: 'Auction', sublabel: 'auction.appmodel.nl', icon: '⚡', type: 'core', shape: 'server' },
+ { id: 'MI50', label: 'MI50/Ollama', sublabel: 'ollama.lan', icon: '🧠', type: 'core', shape: 'server' },
+
+ // Infra
+ { id: 'AdGuard', label: 'AdGuard', sublabel: 'DNS Filter', icon: '🛡️', type: 'infra', shape: 'server' },
+ { id: 'XU4', label: 'XU4 DNS', sublabel: '192.168.1.163', icon: '🔧', type: 'infra', shape: 'chip' },
+ { id: 'C2', label: 'C2 DNS', sublabel: '192.168.1.227', icon: '🔧', type: 'infra', shape: 'chip' },
+ { id: 'HA', label: 'Home Assistant', sublabel: '192.168.1.193', icon: '🏠', type: 'infra', shape: 'server' },
+
+ // Workers / devices
+ { id: 'Atlas', label: 'Atlas', sublabel: '192.168.1.100', icon: '💻', type: 'worker', shape: 'chip' },
+ { id: 'Hermes', label: 'Hermes', sublabel: '192.168.137.239', icon: '💻', type: 'worker', shape: 'chip' },
+ { id: 'Plato', label: 'Plato', sublabel: 'llm.plato.lan', icon: '💻', type: 'worker', shape: 'chip' },
+
+ // IoT and clients
+ { id: 'TV', label: 'Kamer-TV', sublabel: '192.168.1.240', icon: '📺', type: 'iot', shape: 'monitor' },
+ { id: 'Hue', label: 'Philips Hue', sublabel: '192.168.1.49', icon: '💡', type: 'iot', shape: 'server' },
+ { id: 'Eufy', label: 'Eufy S380HB', sublabel: '192.168.1.59', icon: '📹', type: 'iot', shape: 'server' },
+ { id: 'IoT', label: 'IoT Devices', sublabel: 'Various', icon: '🔌', type: 'iot', shape: 'server' },
+ { id: 'MIKE', label: 'MIKE PC', sublabel: '192.168.1.100', icon: '🖥️', type: 'client', shape: 'monitor' },
+ { id: 'Lotte', label: 'Lotte', sublabel: '192.168.1.133', icon: '📱', type: 'client', shape: 'monitor' }
];
const links = [