This commit is contained in:
Tour
2025-12-08 21:19:34 +01:00
parent 730359e67a
commit 3382c5e1cd
7 changed files with 3409 additions and 2 deletions

933
public/top.html Normal file
View File

@@ -0,0 +1,933 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive Network Topology</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
color: #e2e8f0;
overflow: hidden;
}
.container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: rgba(15, 23, 42, 0.9);
backdrop-filter: blur(10px);
padding: 20px;
border-bottom: 1px solid #334155;
z-index: 10;
}
.header h1 {
font-size: 1.8em;
background: linear-gradient(90deg, #60a5fa 0%, #818cf8 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
display: flex;
align-items: center;
gap: 12px;
}
.controls {
position: absolute;
top: 20px;
right: 20px;
display: flex;
gap: 10px;
z-index: 100;
}
.control-btn {
background: rgba(30, 41, 59, 0.7);
border: 1px solid #475569;
color: #cbd5e1;
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.control-btn:hover {
background: rgba(56, 70, 100, 0.8);
border-color: #60a5fa;
}
.main-svg {
flex: 1;
width: 100%;
height: calc(100% - 80px);
cursor: grab;
}
.main-svg:active {
cursor: grabbing;
}
/* Node Styles */
.node {
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.3));
}
.node:hover {
transform: scale(1.1);
filter: drop-shadow(0 0 15px currentColor);
}
.node.selected {
transform: scale(1.15);
filter: drop-shadow(0 0 20px currentColor);
}
.node-circle {
stroke-width: 2;
transition: stroke-width 0.3s ease;
}
.node:hover .node-circle {
stroke-width: 3;
}
.node-text {
font-size: 12px;
font-weight: 600;
text-anchor: middle;
pointer-events: none;
user-select: none;
}
.node-subtext {
font-size: 10px;
fill: #94a3b8;
text-anchor: middle;
pointer-events: none;
}
/* Connection Styles */
.connection {
fill: none;
stroke: #475569;
stroke-width: 2;
cursor: pointer;
transition: all 0.3s ease;
}
.connection:hover {
stroke: #60a5fa;
stroke-width: 3;
}
.connection.highlighted {
stroke: #818cf8;
stroke-width: 3;
filter: drop-shadow(0 0 5px #818cf8);
}
/* Subnet Containers */
.subnet-container {
fill: none;
stroke: #334155;
stroke-width: 2;
stroke-dasharray: 5,5;
rx: 15;
ry: 15;
}
.subnet-label {
font-size: 14px;
font-weight: bold;
fill: #94a3b8;
text-anchor: middle;
}
/* Legend */
.legend {
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(15, 23, 42, 0.8);
padding: 15px;
border-radius: 12px;
backdrop-filter: blur(10px);
border: 1px solid #334155;
font-size: 11px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
cursor: pointer;
transition: opacity 0.3s ease;
}
.legend-item:hover {
opacity: 0.8;
}
.legend-color {
width: 14px;
height: 14px;
border-radius: 50%;
border: 2px solid;
}
/* Tooltip */
.tooltip {
position: fixed;
background: rgba(15, 23, 42, 0.95);
color: #e2e8f0;
padding: 12px 16px;
border-radius: 8px;
font-size: 12px;
pointer-events: none;
z-index: 1000;
opacity: 0;
transition: opacity 0.3s ease;
border: 1px solid #475569;
backdrop-filter: blur(10px);
max-width: 300px;
}
.tooltip.show {
opacity: 1;
}
/* Detail Panel */
.detail-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.9);
background: rgba(15, 23, 42, 0.98);
border: 1px solid #475569;
border-radius: 16px;
padding: 30px;
min-width: 400px;
max-width: 600px;
z-index: 2000;
opacity: 0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(20px);
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
}
.detail-panel.show {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #334155;
}
.detail-title {
font-size: 1.5em;
color: #60a5fa;
}
.close-btn {
background: none;
border: none;
color: #94a3b8;
font-size: 24px;
cursor: pointer;
transition: color 0.3s ease;
}
.close-btn:hover {
color: #e2e8f0;
}
.detail-content {
line-height: 1.6;
}
.detail-section {
margin-bottom: 20px;
}
.detail-section h3 {
color: #818cf8;
margin-bottom: 10px;
font-size: 1.1em;
}
.tag {
display: inline-block;
background: rgba(96, 165, 250, 0.2);
color: #60a5fa;
padding: 4px 10px;
border-radius: 6px;
font-size: 11px;
margin: 2px;
border: 1px solid rgba(96, 165, 250, 0.3);
}
.warning {
color: #f87171;
border-color: rgba(248, 113, 113, 0.3);
background: rgba(248, 113, 113, 0.1);
}
/* Overlay */
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 1500;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.overlay.show {
opacity: 1;
pointer-events: all;
}
/* Animation for active nodes */
@keyframes pulse {
0% { r: 15; }
50% { r: 18; opacity: 0.7; }
100% { r: 15; }
}
.node.active .node-circle {
animation: pulse 2s infinite;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🌐 Interactive Network Topology</h1>
</div>
<div class="controls">
<button class="control-btn" onclick="resetView()">🎯 Reset View</button>
<button class="control-btn" onclick="toggleAnimation()">✨ Animate</button>
<button class="control-btn" onclick="exportSVG()">💾 Export</button>
</div>
<svg class="main-svg" id="network-svg" viewBox="0 0 1600 900">
<defs>
<!-- Gradient definitions -->
<linearGradient id="gradient-router" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#f59e0b;stop-opacity:1" />
<stop offset="100%" style="stop-color:#fbbf24;stop-opacity:1" />
</linearGradient>
<linearGradient id="gradient-core" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#ef4444;stop-opacity:1" />
<stop offset="100%" style="stop-color:#f87171;stop-opacity:1" />
</linearGradient>
<linearGradient id="gradient-infra" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#10b981;stop-opacity:1" />
<stop offset="100%" style="stop-color:#34d399;stop-opacity:1" />
</linearGradient>
<linearGradient id="gradient-worker" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#8b5cf6;stop-opacity:1" />
<stop offset="100%" style="stop-color:#a78bfa;stop-opacity:1" />
</linearGradient>
<linearGradient id="gradient-iot" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#f97316;stop-opacity:1" />
<stop offset="100%" style="stop-color:#fb923c;stop-opacity:1" />
</linearGradient>
<linearGradient id="gradient-client" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#6b7280;stop-opacity:1" />
<stop offset="100%" style="stop-color:#9ca3af;stop-opacity:1" />
</linearGradient>
<linearGradient id="gradient-dns" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#0ea5e9;stop-opacity:1" />
<stop offset="100%" style="stop-color:#38bdf8;stop-opacity:1" />
</linearGradient>
<!-- Filter for glow effects -->
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- Subnet containers -->
<rect class="subnet-container" x="50" y="200" width="1100" height="650" />
<text class="subnet-label" x="600" y="230">LAN 192.168.1.0/24</text>
<rect class="subnet-container" x="1200" y="400" width="350" height="350" />
<text class="subnet-label" x="1375" y="430">Tether_LAN_192.168.137</text>
<!-- Nodes -->
<g class="node" data-id="net" data-type="internet" transform="translate(800, 50)">
<circle class="node-circle" r="30" fill="url(#gradient-dns)" stroke="#0ea5e9" />
<text class="node-text" y="5">☁️ Internet</text>
<text class="node-subtext" y="45">5.132.33.195</text>
</g>
<g class="node" data-id="router" data-type="router" transform="translate(800, 150)">
<rect class="node-circle" x="-35" y="-20" width="70" height="40" rx="5" fill="url(#gradient-router)" stroke="#f59e0b" />
<text class="node-text" y="5">🛜 Router</text>
<text class="node-subtext" y="50">hub.lan</text>
<text class="node-subtext" y="65">192.168.1.1</text>
</g>
<!-- CORE SERVER Group -->
<g class="node-group" data-group="core">
<g class="node" data-id="traefik" data-type="core" transform="translate(200, 300)">
<circle class="node-circle" r="20" fill="url(#gradient-core)" stroke="#ef4444" />
<text class="node-text" y="25">Traefik</text>
<text class="node-subtext" y="38">Reverse Proxy</text>
</g>
<g class="node" data-id="dokku" data-type="core" transform="translate(350, 300)">
<circle class="node-circle" r="20" fill="url(#gradient-core)" stroke="#ef4444" />
<text class="node-text" y="25">Dokku PaaS</text>
</g>
<g class="node" data-id="gitea" data-type="core" transform="translate(275, 380)">
<circle class="node-circle" r="20" fill="url(#gradient-core)" stroke="#ef4444" />
<text class="node-text" y="25">Gitea</text>
<text class="node-subtext" y="38">git.appmodel.nl</text>
</g>
<g class="node" data-id="auction" data-type="core" transform="translate(150, 380)">
<circle class="node-circle" r="20" fill="url(#gradient-core)" stroke="#ef4444" />
<text class="node-text" y="25">Auction Stack</text>
<text class="node-subtext" y="38">auction.appmodel.nl</text>
</g>
<g class="node" data-id="mi50" data-type="core" transform="translate(400, 380)">
<circle class="node-circle" r="20" fill="url(#gradient-core)" stroke="#ef4444" />
<text class="node-text" y="25">MI50 / Ollama</text>
<text class="node-subtext" y="38">ollama.lan</text>
</g>
</g>
<!-- INFRA Group -->
<g class="node-group" data-group="infra">
<g class="node" data-id="xu4" data-type="infra" transform="translate(600, 320)">
<circle class="node-circle" r="25" fill="url(#gradient-infra)" stroke="#10b981" />
<text class="node-text" y="5">XU4 DNS</text>
<text class="node-subtext" y="55">192.168.1.163</text>
</g>
<g class="node" data-id="cu2" data-type="infra" transform="translate(750, 320)">
<circle class="node-circle" r="25" fill="url(#gradient-infra)" stroke="#10b981" />
<text class="node-text" y="5">C2 DNS</text>
<text class="node-subtext" y="55">192.168.1.227</text>
</g>
<g class="node" data-id="adguard" data-type="infra" transform="translate(675, 400)">
<circle class="node-circle" r="25" fill="url(#gradient-infra)" stroke="#10b981" />
<text class="node-text" y="5">AdGuard DNS</text>
</g>
</g>
<!-- Workers -->
<g class="node" data-id="atlas" data-type="worker" transform="translate(900, 320)">
<circle class="node-circle" r="25" fill="url(#gradient-worker)" stroke="#8b5cf6" />
<text class="node-text" y="5">Atlas</text>
<text class="node-subtext" y="55">192.168.1.100</text>
</g>
<g class="node" data-id="ha" data-type="infra" transform="translate(1050, 320)">
<circle class="node-circle" r="25" fill="url(#gradient-infra)" stroke="#10b981" />
<text class="node-text" y="5">Home Assistant</text>
<text class="node-subtext" y="55">192.168.1.193</text>
</g>
<!-- IoT Devices -->
<g class="node" data-id="iot_tv" data-type="iot" transform="translate(200, 550)">
<circle class="node-circle" r="20" fill="url(#gradient-iot)" stroke="#f97316" />
<text class="node-text" y="25">📺 Kamer-TV</text>
<text class="node-subtext" y="38">192.168.1.240</text>
</g>
<g class="node" data-id="iot_hue" data-type="iot" transform="translate(350, 550)">
<circle class="node-circle" r="20" fill="url(#gradient-iot)" stroke="#f97316" />
<text class="node-text" y="25">💡 Philips Hue</text>
<text class="node-subtext" y="38">192.168.1.49</text>
</g>
<g class="node" data-id="iot_eufy" data-type="iot" transform="translate(500, 550)">
<circle class="node-circle" r="20" fill="url(#gradient-iot)" stroke="#f97316" />
<text class="node-text" y="25">📹 Eufy S380HB</text>
<text class="node-subtext" y="38">192.168.1.59</text>
</g>
<g class="node" data-id="iot_misc" data-type="iot" transform="translate(650, 550)">
<circle class="node-circle" r="20" fill="url(#gradient-iot)" stroke="#f97316" />
<text class="node-text" y="25">🔌 IoT Devices</text>
<text class="node-subtext" y="38">Nest / Roborock / ESP / Printer</text>
</g>
<!-- Clients -->
<g class="node" data-id="clientMike" data-type="client" transform="translate(900, 550)">
<rect class="node-circle" x="-40" y="-20" width="80" height="40" rx="5" fill="url(#gradient-client)" stroke="#6b7280" />
<text class="node-text" y="5">💻 MIKE PC</text>
<text class="node-subtext" y="50">192.168.1.100</text>
<text class="node-subtext" y="65">LAN + Tether gateway</text>
</g>
<g class="node" data-id="clientLotte" data-type="client" transform="translate(1100, 550)">
<circle class="node-circle" r="20" fill="url(#gradient-client)" stroke="#6b7280" />
<text class="node-text" y="25">📱 Lotte</text>
<text class="node-subtext" y="38">192.168.1.133</text>
</g>
<!-- Tether Subnet -->
<g class="node" data-id="hermes" data-type="worker" transform="translate(1300, 500)">
<circle class="node-circle" r="20" fill="url(#gradient-worker)" stroke="#8b5cf6" />
<text class="node-text" y="25">Hermes</text>
<text class="node-subtext" y="38">192.168.137.239</text>
</g>
<g class="node" data-id="plato" data-type="worker" transform="translate(1450, 500)">
<circle class="node-circle" r="20" fill="url(#gradient-worker)" stroke="#8b5cf6" />
<text class="node-text" y="25">Plato</text>
<text class="node-subtext" y="38">192.168.137.239</text>
<text class="node-subtext" y="51">llm.plato.lan</text>
</g>
<!-- Connections -->
<g id="connections">
<!-- Main connections -->
<path class="connection" d="M 800 80 L 800 130" marker-end="url(#arrowhead)" />
<path class="connection" d="M 800 190 Q 500 250 200 300" />
<path class="connection" d="M 800 190 Q 700 250 600 320" />
<path class="connection" d="M 800 190 Q 850 250 900 320" />
<path class="connection" d="M 800 190 Q 950 250 1050 320" />
<path class="connection" d="M 800 190 Q 500 400 200 550" />
<path class="connection" d="M 800 190 Q 600 400 350 550" />
<path class="connection" d="M 800 190 Q 700 400 500 550" />
<path class="connection" d="M 800 190 Q 800 400 650 550" />
<path class="connection" d="M 800 190 Q 850 400 900 550" />
<path class="connection" d="M 800 190 Q 950 400 1100 550" />
<!-- Tether bridge -->
<path class="connection" d="M 940 570 L 1300 500" stroke-dasharray="5,5" />
<!-- Internal group connections -->
<path class="connection" d="M 220 320 Q 300 320 330 320" />
<path class="connection" d="M 220 320 Q 240 350 260 380" />
<path class="connection" d="M 220 320 Q 180 350 170 380" />
<path class="connection" d="M 330 320 Q 350 350 370 380" />
<path class="connection" d="M 620 345 Q 640 360 660 375" />
<path class="connection" d="M 730 345 Q 710 360 690 375" />
</g>
<!-- Marker definition for arrows -->
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#475569" />
</marker>
</defs>
</svg>
<div class="legend">
<div class="legend-item" data-type="internet">
<div class="legend-color" style="border-color: #0ea5e9; background: linear-gradient(135deg, #0ea5e9 0%, #38bdf8 100%);"></div>
<span>Internet/DNS</span>
</div>
<div class="legend-item" data-type="router">
<div class="legend-color" style="border-color: #f59e0b; background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%);"></div>
<span>Router</span>
</div>
<div class="legend-item" data-type="core">
<div class="legend-color" style="border-color: #ef4444; background: linear-gradient(135deg, #ef4444 0%, #f87171 100%);"></div>
<span>Core Server</span>
</div>
<div class="legend-item" data-type="infra">
<div class="legend-color" style="border-color: #10b981; background: linear-gradient(135deg, #10b981 0%, #34d399 100%);"></div>
<span>Infra/DNS</span>
</div>
<div class="legend-item" data-type="worker">
<div class="legend-color" style="border-color: #8b5cf6; background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%);"></div>
<span>Worker/Edge</span>
</div>
<div class="legend-item" data-type="iot">
<div class="legend-color" style="border-color: #f97316; background: linear-gradient(135deg, #f97316 0%, #fb923c 100%);"></div>
<span>IoT Devices</span>
</div>
<div class="legend-item" data-type="client">
<div class="legend-color" style="border-color: #6b7280; background: linear-gradient(135deg, #6b7280 0%, #9ca3af 100%);"></div>
<span>Clients</span>
</div>
</div>
</div>
<div class="tooltip" id="tooltip"></div>
<div class="overlay" id="overlay" onclick="closePanel()"></div>
<div class="detail-panel" id="detail-panel">
<div class="detail-header">
<div class="detail-title" id="detail-title">Node Details</div>
<button class="close-btn" onclick="closePanel()">×</button>
</div>
<div class="detail-content" id="detail-content"></div>
</div>
<script>
const networkData = {
net: { name: 'Internet / Cloudflare DNS', ip: '5.132.33.195', type: 'internet', desc: 'External network and DNS provider' },
router: { name: 'Zyxel VMG8825-T50', ip: '192.168.1.1', hostname: 'hub.lan', type: 'router', desc: 'Main gateway router' },
// Core Server Group
traefik: { name: 'Traefik', type: 'core', desc: 'Reverse Proxy', services: ['Reverse Proxy', 'Load Balancer'] },
dokku: { name: 'Dokku PaaS', type: 'core', desc: 'Platform as a Service', services: ['Docker Management', 'App Deployment'] },
gitea: { name: 'Gitea', ip: '192.168.1.159', type: 'core', desc: 'Self-hosted Git service', hostname: 'git.appmodel.nl' },
auction: { name: 'Auction Stack', ip: '192.168.1.159', type: 'core', desc: 'Auction application', hostname: 'auction.appmodel.nl' },
mi50: { name: 'MI50 / Ollama', ip: '192.168.1.159', type: 'core', desc: 'AI/ML inference server', hostname: 'ollama.lan' },
// Infra Group
xu4: { name: 'XU4 DNS', ip: '192.168.1.163', type: 'infra', desc: 'Primary DNS server (Odroid XU4)', services: ['DNS', 'Pi-hole/AdGuard'] },
cu2: { name: 'C2 DNS', ip: '192.168.1.227', type: 'infra', desc: 'Secondary DNS server (Odroid C2)', services: ['DNS'] },
adguard: { name: 'AdGuard DNS', type: 'infra', desc: 'Network-wide ad blocking', services: ['DNS Filtering', 'Ad Blocking'] },
// Workers
atlas: { name: 'Atlas', ip: '192.168.1.100', type: 'worker', desc: 'Worker node', services: ['Docker', 'Portainer'] },
ha: { name: 'Home Assistant', ip: '192.168.1.193', type: 'infra', desc: 'Home automation platform', services: ['Automation', 'IoT Control'] },
// IoT Devices
iot_tv: { name: 'Kamer-TV', ip: '192.168.1.240', type: 'iot', desc: 'Chromecast/Smart TV', device: 'Smart TV' },
iot_hue: { name: 'Philips Hue', ip: '192.168.1.49', type: 'iot', desc: 'Smart lighting bridge' },
iot_eufy: { name: 'Eufy S380HB', ip: '192.168.1.59', type: 'iot', desc: 'Security camera', device: 'Beveiligingscamera' },
iot_misc: { name: 'IoT Devices', type: 'iot', desc: 'Various IoT devices', devices: ['Nest Mini', 'Roborock Vacuum', 'ESP devices', 'HP Printer'] },
// Clients
clientMike: { name: 'MIKE PC', ip: '192.168.1.100', type: 'client', desc: 'Workstation with tether gateway', interfaces: ['LAN', 'Tether Bridge'] },
clientLotte: { name: 'Lotte', ip: '192.168.1.133', type: 'client', desc: 'Mobile device' },
// Tether
hermes: { name: 'Hermes', ip: '192.168.137.239', type: 'worker', desc: 'Tether network node' },
plato: { name: 'Plato', ip: '192.168.137.239', type: 'worker', desc: 'LLM server', hostname: 'llm.plato.lan' }
};
let selectedNode = null;
let isAnimating = true;
let svgPanZoom;
// Initialize
document.addEventListener('DOMContentLoaded', function() {
initializeSVG();
setupEventListeners();
startAnimation();
});
function initializeSVG() {
const svg = document.getElementById('network-svg');
// Add pan and zoom
svgPanZoom = svgPanZoomInstance(svg, {
zoomEnabled: true,
controlIconsEnabled: false,
fit: true,
center: true,
minZoom: 0.5,
maxZoom: 3
});
}
function setupEventListeners() {
// Node click events
document.querySelectorAll('.node').forEach(node => {
node.addEventListener('click', (e) => {
e.stopPropagation();
const nodeId = node.dataset.id;
selectNode(nodeId);
});
node.addEventListener('mouseenter', (e) => {
const nodeId = node.dataset.id;
const data = networkData[nodeId];
showTooltip(e, data);
highlightConnections(nodeId);
});
node.addEventListener('mouseleave', () => {
hideTooltip();
unhighlightConnections();
});
});
// Legend interactions
document.querySelectorAll('.legend-item').forEach(item => {
item.addEventListener('click', () => {
const type = item.dataset.type;
toggleNodeType(type);
});
});
// Connection hover
document.querySelectorAll('.connection').forEach(conn => {
conn.addEventListener('mouseenter', (e) => {
conn.classList.add('highlighted');
});
conn.addEventListener('mouseleave', (e) => {
conn.classList.remove('highlighted');
});
});
}
function selectNode(nodeId) {
// Remove previous selection
document.querySelectorAll('.node').forEach(n => n.classList.remove('selected'));
// Select new node
const node = document.querySelector(`[data-id="${nodeId}"]`);
if (node) {
node.classList.add('selected');
selectedNode = nodeId;
showDetailPanel(nodeId);
}
}
function showTooltip(event, data) {
const tooltip = document.getElementById('tooltip');
const services = data.services ? data.services.join(', ') : '';
const devices = data.devices ? data.devices.join(', ') : '';
tooltip.innerHTML = `
<strong>${data.name}</strong><br>
${data.ip ? `IP: ${data.ip}<br>` : ''}
${data.hostname ? `Host: ${data.hostname}<br>` : ''}
${data.desc ? `${data.desc}<br>` : ''}
${services ? `<small>Services: ${services}</small><br>` : ''}
${devices ? `<small>Devices: ${devices}</small>` : ''}
`;
tooltip.style.left = event.clientX + 15 + 'px';
tooltip.style.top = event.clientY - 10 + 'px';
tooltip.classList.add('show');
}
function hideTooltip() {
document.getElementById('tooltip').classList.remove('show');
}
function highlightConnections(nodeId) {
// This would parse the diagram to highlight related connections
// For simplicity, we'll just highlight all connections temporarily
document.querySelectorAll('.connection').forEach(conn => {
conn.style.stroke = '#60a5fa';
conn.style.strokeWidth = '3';
});
}
function unhighlightConnections() {
document.querySelectorAll('.connection').forEach(conn => {
conn.style.stroke = '';
conn.style.strokeWidth = '';
});
}
function showDetailPanel(nodeId) {
const data = networkData[nodeId];
const panel = document.getElementById('detail-panel');
const overlay = document.getElementById('overlay');
const title = document.getElementById('detail-title');
const content = document.getElementById('detail-content');
title.textContent = data.name;
let html = `
<div class="detail-section">
<p>${data.desc || 'No description available'}</p>
</div>
`;
if (data.ip) {
html += `
<div class="detail-section">
<h3>📡 Network Information</h3>
<div><strong>IP Address:</strong> <code class="tag">${data.ip}</code></div>
${data.hostname ? `<div><strong>Hostname:</strong> <code class="tag">${data.hostname}</code></div>` : ''}
</div>
`;
}
if (data.services) {
html += `
<div class="detail-section">
<h3>🚀 Services</h3>
${data.services.map(s => `<span class="tag">${s}</span>`).join('')}
</div>
`;
}
if (data.interfaces) {
html += `
<div class="detail-section">
<h3>🔌 Interfaces</h3>
${data.interfaces.map(i => `<span class="tag">${i}</span>`).join('')}
</div>
`;
}
if (data.devices) {
html += `
<div class="detail-section">
<h3>📱 Sub-Devices</h3>
${data.devices.map(d => `<span class="tag">${d}</span>`).join('')}
</div>
`;
}
// Add related connections
html += `
<div class="detail-section">
<h3>🔗 Connections</h3>
<p style="color: #94a3b8; font-size: 0.9em;">
Click on connections in the diagram to see details
</p>
</div>
`;
content.innerHTML = html;
overlay.classList.add('show');
panel.classList.add('show');
}
function closePanel() {
document.getElementById('overlay').classList.remove('show');
document.getElementById('detail-panel').classList.remove('show');
document.querySelectorAll('.node').forEach(n => n.classList.remove('selected'));
selectedNode = null;
}
function toggleNodeType(type) {
document.querySelectorAll(`.node[data-type="${type}"]`).forEach(node => {
const isVisible = node.style.display !== 'none';
node.style.display = isVisible ? 'none' : 'block';
node.style.opacity = isVisible ? '0' : '1';
});
}
function resetView() {
svgPanZoom.reset();
closePanel();
}
function toggleAnimation() {
isAnimating = !isAnimating;
if (isAnimating) {
startAnimation();
} else {
stopAnimation();
}
}
function startAnimation() {
document.querySelectorAll('.node[data-type="core"], .node[data-type="infra"]').forEach(node => {
node.classList.add('active');
});
}
function stopAnimation() {
document.querySelectorAll('.node').forEach(node => {
node.classList.remove('active');
});
}
function exportSVG() {
const svg = document.getElementById('network-svg');
const svgData = new XMLSerializer().serializeToString(svg);
const blob = new Blob([svgData], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'network-topology.svg';
a.click();
URL.revokeObjectURL(url);
}
// Simple pan/zoom implementation (if you don't want to use a library)
function svgPanZoomInstance(svg, options) {
let isPanning = false;
let startPoint = { x: 0, y: 0 };
let startTranslate = { x: 0, y: 0 };
let scale = 1;
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
g.id = 'pan-zoom-group';
while (svg.firstChild) {
g.appendChild(svg.firstChild);
}
svg.appendChild(g);
svg.addEventListener('mousedown', (e) => {
if (e.target === svg || e.target.tagName === 'path') {
isPanning = true;
startPoint = { x: e.clientX, y: e.clientY };
const transform = g.getAttribute('transform') || '';
const matches = transform.match(/translate\(([^,]+),([^)]+)\)/);
if (matches) {
startTranslate = { x: parseFloat(matches[1]), y: parseFloat(matches[2]) };
}
svg.style.cursor = 'grabbing';
}
});
svg.addEventListener('mousemove', (e) => {
if (!isPanning) return;
const dx = e.clientX - startPoint.x;
const dy = e.clientY - startPoint.y;
const newX = startTranslate.x + dx;
const newY = startTranslate.y + dy;
updateTransform(newX, newY, scale);
});
svg.addEventListener('mouseup', () => {
isPanning = false;
svg.style.cursor = 'grab';
});
svg.addEventListener('wheel', (e) => {
e.preventDefault();
const delta = e.deltaY > 0 ? 0.9 : 1.1;
scale = Math.max(options.minZoom, Math.min(options.maxZoom, scale * delta));
updateTransform(0, 0, scale);
});
function updateTransform(x, y, s) {
g.setAttribute('transform', `translate(${x},${y}) scale(${s})`);
}
return {
reset: () => {
scale = 1;
updateTransform(0, 0, 1);
}
};
}
</script>
</body>
</html>