Files
nex/public/top.html
2025-12-08 21:19:34 +01:00

933 lines
36 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>