fgdf
This commit is contained in:
300
public/d2.html
300
public/d2.html
@@ -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 = [
|
||||||
|
|||||||
Reference in New Issue
Block a user