This commit is contained in:
Tour
2025-12-08 21:28:18 +01:00
parent 3382c5e1cd
commit 7d5194d6fc

View File

@@ -269,6 +269,19 @@
filter: drop-shadow(0 4px 12px rgba(96, 165, 250, 0.6)); filter: drop-shadow(0 4px 12px rgba(96, 165, 250, 0.6));
transform: scale(1.1); 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 { .node-label {
pointer-events: none; pointer-events: none;
user-select: none; user-select: none;
@@ -309,19 +322,42 @@
/* Network Zones */ /* Network Zones */
.network-zone { .network-zone {
fill: rgba(30, 41, 59, 0.3); stroke-width: 3;
stroke: #475569;
stroke-width: 2;
stroke-dasharray: 10,5; stroke-dasharray: 10,5;
rx: 20; rx: 20;
ry: 20; ry: 20;
pointer-events: none; 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 { .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; fill: #94a3b8;
font-size: 14px; font-size: 10px;
font-weight: 600; font-weight: 500;
text-anchor: middle; text-anchor: start;
pointer-events: none; pointer-events: none;
} }
@@ -516,21 +552,45 @@
// Add network zone backgrounds (will be positioned after simulation) // Add network zone backgrounds (will be positioned after simulation)
const zonesGroup = g.append('g').attr('class', 'zones'); 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') const lanZone = zonesGroup.append('rect')
.attr('class', 'network-zone lan-zone') .attr('class', 'network-zone zone-lan');
.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)');
const lanLabel = zonesGroup.append('text') const lanLabel = zonesGroup.append('text')
.attr('class', 'zone-label') .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') const tetherLabel = zonesGroup.append('text')
.attr('class', 'zone-label') .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 // Create arrow markers for directed edges
svg.append('defs').selectAll('marker') svg.append('defs').selectAll('marker')
@@ -567,15 +627,12 @@
.on('drag', dragged) .on('drag', dragged)
.on('end', dragended)); .on('end', dragended));
// Add circles to nodes // Draw meaningful shapes per node instead of circles
node.append('circle') node.each(function(d) {
.attr('class', 'node-circle') drawNodeShape(d3.select(this), d);
.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);
// Add icon/emoji to nodes // Add icon/emoji to nodes (centered)
node.append('text') node.append('text')
.attr('class', 'node-icon') .attr('class', 'node-icon')
.attr('text-anchor', 'middle') .attr('text-anchor', 'middle')
@@ -624,16 +681,23 @@
} }
}); });
// Create force simulation // Create force simulation with better positioning
simulation = d3.forceSimulation(graphData.nodes) simulation = d3.forceSimulation(graphData.nodes)
.force('link', d3.forceLink(graphData.links) .force('link', d3.forceLink(graphData.links)
.id(d => d.id) .id(d => d.id)
.distance(d => d.distance || 150)) .distance(d => d.distance || 150)
.strength(0.5))
.force('charge', d3.forceManyBody() .force('charge', d3.forceManyBody()
.strength(-800)) .strength(-1000)
.distanceMax(400))
.force('center', d3.forceCenter(width / 2, height / 2)) .force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide() .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); .on('tick', ticked);
function ticked() { function ticked() {
@@ -659,6 +723,140 @@
d.fy = d.y; 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) { function dragged(event, d) {
d.fx = event.x; d.fx = event.x;
d.fy = event.y; d.fy = event.y;
@@ -688,26 +886,34 @@
// Create graph data structure from network data // Create graph data structure from network data
function createGraphData() { function createGraphData() {
const nodes = [ const nodes = [
{ id: 'Internet', label: 'Internet', sublabel: '5.132.33.195', icon: '☁️', type: 'dns' }, { 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' }, { id: 'Router', label: 'Zyxel Router', sublabel: '192.168.1.1', icon: '🛜', type: 'router', shape: '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' }, // Application stack (Traefik with nginx-like hexagon)
{ id: 'Gitea', label: 'Gitea', sublabel: 'git.appmodel.nl', icon: '', type: 'core' }, { id: 'Traefik', label: 'Traefik', sublabel: '192.168.1.159', icon: 'N', type: 'core', shape: 'hex' },
{ id: 'Auction', label: 'Auction', sublabel: 'auction.appmodel.nl', icon: '⚡', type: 'core' }, { id: 'Dokku', label: 'Dokku', sublabel: '192.168.1.159', icon: '⚡', type: 'core', shape: 'server' },
{ id: 'MI50', label: 'MI50/Ollama', sublabel: 'ollama.lan', icon: '⚡', type: 'core' }, { id: 'Gitea', label: 'Gitea', sublabel: 'git.appmodel.nl', icon: '⚡', type: 'core', shape: 'server' },
{ id: 'AdGuard', label: 'AdGuard', sublabel: 'DNS Filter', icon: '🔧', type: 'infra' }, { id: 'Auction', label: 'Auction', sublabel: 'auction.appmodel.nl', icon: '', type: 'core', shape: 'server' },
{ id: 'XU4', label: 'XU4 DNS', sublabel: '192.168.1.163', icon: '🔧', type: 'infra' }, { id: 'MI50', label: 'MI50/Ollama', sublabel: 'ollama.lan', icon: '🧠', type: 'core', shape: 'server' },
{ 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' }, // Infra
{ id: 'Atlas', label: 'Atlas', sublabel: '192.168.1.100', icon: '💻', type: 'worker' }, { id: 'AdGuard', label: 'AdGuard', sublabel: 'DNS Filter', icon: '🛡️', type: 'infra', shape: 'server' },
{ id: 'TV', label: 'Kamer-TV', sublabel: '192.168.1.240', icon: '📺', type: 'iot' }, { id: 'XU4', label: 'XU4 DNS', sublabel: '192.168.1.163', icon: '🔧', type: 'infra', shape: 'chip' },
{ id: 'Hue', label: 'Philips Hue', sublabel: '192.168.1.49', icon: '💡', type: 'iot' }, { id: 'C2', label: 'C2 DNS', sublabel: '192.168.1.227', icon: '🔧', type: 'infra', shape: 'chip' },
{ id: 'Eufy', label: 'Eufy S380HB', sublabel: '192.168.1.59', icon: '📹', type: 'iot' }, { id: 'HA', label: 'Home Assistant', sublabel: '192.168.1.193', icon: '🏠', type: 'infra', shape: 'server' },
{ id: 'IoT', label: 'IoT Devices', sublabel: 'Various', icon: '🔌', type: 'iot' },
{ id: 'MIKE', label: 'MIKE PC', sublabel: '192.168.1.100', icon: '💻', type: 'client' }, // Workers / devices
{ id: 'Lotte', label: 'Lotte', sublabel: '192.168.1.133', icon: '📱', type: 'client' }, { 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' }, { 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' } { 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 = [ const links = [