// =====================================================
// SentinelAI Network Page Controller
// =====================================================
// ---- Network Nodes ----
const networkNodes = [
{ id: 'internet', name: 'Internet', ip: '0.0.0.0/0', type: 'cloud', status: 'online', x: 0.5, y: 0.08, icon: 'globe' },
{ id: 'router', name: 'Core Router', ip: '10.0.0.1', type: 'router', status: 'online', x: 0.5, y: 0.22, icon: 'router' },
{ id: 'firewall', name: 'Firewall', ip: '10.0.0.2', type: 'firewall', status: 'online', x: 0.5, y: 0.36, icon: 'shield' },
{ id: 'switch1', name: 'Switch-A', ip: '10.0.1.1', type: 'switch', status: 'online', x: 0.25, y: 0.52, icon: 'network' },
{ id: 'switch2', name: 'Switch-B', ip: '10.0.2.1', type: 'switch', status: 'online', x: 0.75, y: 0.52, icon: 'network' },
{ id: 'lb', name: 'Load Balancer', ip: '10.0.0.50', type: 'lb', status: 'online', x: 0.5, y: 0.52, icon: 'shuffle' },
{ id: 'web1', name: 'Web-01', ip: '10.0.1.15', type: 'server', status: 'online', x: 0.12, y: 0.72, icon: 'server' },
{ id: 'web2', name: 'Web-02', ip: '10.0.1.16', type: 'server', status: 'online', x: 0.38, y: 0.72, icon: 'server' },
{ id: 'db1', name: 'DB-Master', ip: '10.0.2.10', type: 'database', status: 'warning', x: 0.62, y: 0.72, icon: 'database' },
{ id: 'db2', name: 'DB-Slave', ip: '10.0.2.11', type: 'database', status: 'online', x: 0.88, y: 0.72, icon: 'database' },
{ id: 'api', name: 'API-GW', ip: '10.0.1.50', type: 'server', status: 'online', x: 0.5, y: 0.88, icon: 'code' },
{ id: 'backup', name: 'Backup-SRV', ip: '10.0.3.20', type: 'server', status: 'offline', x: 0.15, y: 0.88, icon: 'archive' },
];
// ---- Network Connections ----
const networkLinks = [
{ from: 'internet', to: 'router' },
{ from: 'router', to: 'firewall' },
{ from: 'firewall', to: 'switch1' },
{ from: 'firewall', to: 'switch2' },
{ from: 'firewall', to: 'lb' },
{ from: 'switch1', to: 'web1' },
{ from: 'switch1', to: 'web2' },
{ from: 'switch2', to: 'db1' },
{ from: 'switch2', to: 'db2' },
{ from: 'lb', to: 'web1' },
{ from: 'lb', to: 'web2' },
{ from: 'lb', to: 'api' },
{ from: 'web1', to: 'api' },
{ from: 'api', to: 'db1' },
{ from: 'switch1', to: 'backup' },
];
// ---- Port Scan Data ----
const portScanData = [
{ asset: 'Web-01', ip: '10.0.1.15', port: 22, service: 'SSH', proto: 'TCP', state: 'open', banner: 'OpenSSH 9.0' },
{ asset: 'Web-01', ip: '10.0.1.15', port: 80, service: 'HTTP', proto: 'TCP', state: 'open', banner: 'nginx 1.25.3' },
{ asset: 'Web-01', ip: '10.0.1.15', port: 443, service: 'HTTPS', proto: 'TCP', state: 'open', banner: 'nginx 1.25.3' },
{ asset: 'Web-02', ip: '10.0.1.16', port: 80, service: 'HTTP', proto: 'TCP', state: 'open', banner: 'nginx 1.25.3' },
{ asset: 'Web-02', ip: '10.0.1.16', port: 443, service: 'HTTPS', proto: 'TCP', state: 'open', banner: 'nginx 1.25.3' },
{ asset: 'DB-Master', ip: '10.0.2.10', port: 5432, service: 'PostgreSQL', proto: 'TCP', state: 'open', banner: 'PostgreSQL 16.1' },
{ asset: 'DB-Slave', ip: '10.0.2.11', port: 5432, service: 'PostgreSQL', proto: 'TCP', state: 'open', banner: 'PostgreSQL 16.1' },
{ asset: 'API-GW', ip: '10.0.1.50', port: 8080, service: 'HTTP-Proxy', proto: 'TCP', state: 'open', banner: 'Kong 3.4' },
{ asset: 'API-GW', ip: '10.0.1.50', port: 8443, service: 'HTTPS-Alt', proto: 'TCP', state: 'open', banner: 'Kong 3.4' },
{ asset: 'Core Router', ip: '10.0.0.1', port: 22, service: 'SSH', proto: 'TCP', state: 'filtered', banner: '—' },
{ asset: 'Core Router', ip: '10.0.0.1', port: 161, service: 'SNMP', proto: 'UDP', state: 'open', banner: 'Cisco IOS 15.7' },
{ asset: 'Firewall', ip: '10.0.0.2', port: 443, service: 'HTTPS', proto: 'TCP', state: 'open', banner: 'pfSense 2.7.2' },
{ asset: 'Backup-SRV', ip: '10.0.3.20', port: 22, service: 'SSH', proto: 'TCP', state: 'closed', banner: '—' },
{ asset: 'Backup-SRV', ip: '10.0.3.20', port: 873, service: 'rsync', proto: 'TCP', state: 'closed', banner: '—' },
{ asset: 'Load Balancer', ip: '10.0.0.50', port: 80, service: 'HTTP', proto: 'TCP', state: 'open', banner: 'HAProxy 2.8' },
];
// ---- Connection Templates ----
const connTemplates = [
{ proto: 'TCP', state: 'ESTABLISHED' },
{ proto: 'TCP', state: 'TIME_WAIT' },
{ proto: 'UDP', state: 'ESTABLISHED' },
{ proto: 'TCP', state: 'CLOSE_WAIT' },
{ proto: 'TCP', state: 'ESTABLISHED' },
];
let extIPs = [];
// ---- State ----
let trafficDataIn = [];
let trafficDataOut = [];
let activeConnections = [];
let packetsPerSec = 0;
let totalInGB = 0;
let totalOutGB = 0;
let peakRate = 0;
let topologyAnimId = null;
let packets = [];
let hoveredNode = null;
let canvasScale = 1;
let canvasOffsetX = 0;
let canvasOffsetY = 0;
// ---- Initialize ----
document.addEventListener('DOMContentLoaded', async () => {
lucide.createIcons();
initStats();
initTrafficData();
initConnections();
renderNodeStatus();
renderPortScan();
initTopology();
initTrafficChart();
startClock();
await startRealMonitoring();
});
// ---- Clock ----
function startClock() {
function update() {
const now = new Date();
const timeEl = document.getElementById('systemTime');
if (timeEl) timeEl.textContent = now.toUTCString().split(' ')[4] + ' UTC';
}
update();
setInterval(update, 1000);
}
// ---- Counter Animation ----
function animateCounter(el, target, duration = 1200) {
if (!el) return;
const start = parseInt(el.textContent) || 0;
const startTime = performance.now();
function step(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
el.textContent = Math.floor(start + (target - start) * eased);
if (progress < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
// ---- Init Stats ----
function initStats() {
const online = networkNodes.filter(n => n.status === 'online').length;
const total = networkNodes.length;
animateCounter(document.getElementById('nodesOnline'), online);
const nodesTotalEl = document.getElementById('nodesTotal');
if (nodesTotalEl) nodesTotalEl.textContent = total;
const nodeStatusCountEl = document.getElementById('nodeStatusCount');
if (nodeStatusCountEl) nodeStatusCountEl.textContent = `${online}/${total}`;
const bw = (Math.random() * 5 + 8).toFixed(1);
const bwEl = document.getElementById('bandwidthVal');
if (bwEl) bwEl.textContent = bw;
const latency = Math.floor(Math.random() * 10 + 12);
const latEl = document.getElementById('latencyVal');
if (latEl) latEl.textContent = latency;
const conns = Math.floor(Math.random() * 50 + 180);
animateCounter(document.getElementById('connCount'), conns);
}
// ---- Traffic Data ----
function initTrafficData() {
for (let i = 0; i < 60; i++) {
trafficDataIn.push(Math.random() * 30 + 20);
trafficDataOut.push(Math.random() * 20 + 10);
}
totalInGB = Math.floor(Math.random() * 200 + 800);
totalOutGB = Math.floor(Math.random() * 100 + 400);
peakRate = (Math.random() * 3 + 12).toFixed(1);
updateTrafficStats();
}
function updateTrafficStats() {
const totalInEl = document.getElementById('totalIn');
const totalOutEl = document.getElementById('totalOut');
const peakRateEl = document.getElementById('peakRate');
if (totalInEl) totalInEl.textContent = `${totalInGB} GB`;
if (totalOutEl) totalOutEl.textContent = `${totalOutGB} GB`;
if (peakRateEl) peakRateEl.textContent = `${peakRate} Gbps`;
}
// ---- Traffic Chart ----
function initTrafficChart() {
drawTrafficChart();
setInterval(() => {
trafficDataIn.shift();
trafficDataOut.shift();
const newIn = Math.random() * 35 + 15;
const newOut = Math.random() * 25 + 8;
trafficDataIn.push(newIn);
trafficDataOut.push(newOut);
totalInGB += newIn / 100;
totalOutGB += newOut / 100;
if (newIn > peakRate) peakRate = newIn.toFixed(1);
drawTrafficChart();
updateTrafficStats();
}, 2000);
}
function drawTrafficChart() {
const canvas = document.getElementById('trafficChart');
if (!canvas) return;
const rect = canvas.parentElement.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = 200;
const ctx = canvas.getContext('2d');
const w = canvas.width;
const h = canvas.height;
const dataIn = trafficDataIn;
const dataOut = trafficDataOut;
const maxVal = Math.max(...dataIn, ...dataOut, 50);
const stepX = w / (dataIn.length - 1);
ctx.clearRect(0, 0, w, h);
// Grid lines
ctx.strokeStyle = 'rgba(30, 41, 59, 0.5)';
ctx.lineWidth = 1;
for (let i = 0; i < 4; i++) {
const y = (h / 4) * i;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(w, y);
ctx.stroke();
}
// Inbound fill
const gradIn = ctx.createLinearGradient(0, 0, 0, h);
gradIn.addColorStop(0, 'rgba(6, 182, 212, 0.3)');
gradIn.addColorStop(1, 'rgba(6, 182, 212, 0)');
ctx.beginPath();
ctx.moveTo(0, h);
dataIn.forEach((val, i) => {
const x = i * stepX;
const y = h - (val / maxVal) * (h - 10) - 5;
ctx.lineTo(x, y);
});
ctx.lineTo(w, h);
ctx.closePath();
ctx.fillStyle = gradIn;
ctx.fill();
// Inbound line
ctx.beginPath();
dataIn.forEach((val, i) => {
const x = i * stepX;
const y = h - (val / maxVal) * (h - 10) - 5;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.strokeStyle = 'rgba(6, 182, 212, 0.8)';
ctx.lineWidth = 1.5;
ctx.stroke();
// Outbound fill
const gradOut = ctx.createLinearGradient(0, 0, 0, h);
gradOut.addColorStop(0, 'rgba(168, 85, 247, 0.3)');
gradOut.addColorStop(1, 'rgba(168, 85, 247, 0)');
ctx.beginPath();
ctx.moveTo(0, h);
dataOut.forEach((val, i) => {
const x = i * stepX;
const y = h - (val / maxVal) * (h - 10) - 5;
ctx.lineTo(x, y);
});
ctx.lineTo(w, h);
ctx.closePath();
ctx.fillStyle = gradOut;
ctx.fill();
// Outbound line
ctx.beginPath();
dataOut.forEach((val, i) => {
const x = i * stepX;
const y = h - (val / maxVal) * (h - 10) - 5;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.strokeStyle = 'rgba(168, 85, 247, 0.8)';
ctx.lineWidth = 1.5;
ctx.stroke();
}
// ---- Network Topology Canvas ----
function initTopology() {
const canvas = document.getElementById('topologyCanvas');
if (!canvas) return;
function resize() {
const rect = canvas.parentElement.getBoundingClientRect();
canvas.width = rect.width - 16;
canvas.height = 380;
}
resize();
window.addEventListener('resize', resize);
// Mouse hover
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
hoveredNode = null;
for (const node of networkNodes) {
const nx = node.x * canvas.width;
const ny = node.y * canvas.height;
const dist = Math.sqrt((mx - nx) ** 2 + (my - ny) ** 2);
if (dist < 24) {
hoveredNode = node;
break;
}
}
const tooltip = document.getElementById('topologyTooltip');
if (hoveredNode) {
canvas.style.cursor = 'pointer';
if (tooltip) {
tooltip.classList.remove('hidden');
tooltip.style.left = (mx + 12) + 'px';
tooltip.style.top = (my - 30) + 'px';
const statusColor = hoveredNode.status === 'online' ? 'text-green-400' : hoveredNode.status === 'warning' ? 'text-amber-400' : 'text-red-400';
tooltip.innerHTML = `
${hoveredNode.name}
${hoveredNode.ip}
${hoveredNode.status}
${hoveredNode.type}
`;
}
} else {
canvas.style.cursor = 'grab';
if (tooltip) tooltip.classList.add('hidden');
}
});
canvas.addEventListener('mouseleave', () => {
hoveredNode = null;
const tooltip = document.getElementById('topologyTooltip');
if (tooltip) tooltip.classList.add('hidden');
});
// Spawn initial packets
for (let i = 0; i < 15; i++) {
spawnPacket();
}
animateTopology();
setInterval(spawnPacket, 800);
}
function spawnPacket() {
if (packets.length > 40) packets.shift();
const link = networkLinks[Math.floor(Math.random() * networkLinks.length)];
const fromNode = networkNodes.find(n => n.id === link.from);
const toNode = networkNodes.find(n => n.id === link.to);
if (!fromNode || !toNode) return;
if (fromNode.status === 'offline' || toNode.status === 'offline') return;
packets.push({
fromId: link.from,
toId: link.to,
progress: 0,
speed: Math.random() * 0.01 + 0.005,
color: Math.random() > 0.5 ? '#22d3ee' : '#a78bfa',
});
}
function animateTopology() {
const canvas = document.getElementById('topologyCanvas');
if (!canvas) {
topologyAnimId = requestAnimationFrame(animateTopology);
return;
}
const ctx = canvas.getContext('2d');
const w = canvas.width;
const h = canvas.height;
ctx.clearRect(0, 0, w, h);
// Draw connections
networkLinks.forEach(link => {
const from = networkNodes.find(n => n.id === link.from);
const to = networkNodes.find(n => n.id === link.to);
if (!from || !to) return;
const fx = from.x * w;
const fy = from.y * h;
const tx = to.x * w;
const ty = to.y * h;
const isOffline = from.status === 'offline' || to.status === 'offline';
ctx.beginPath();
ctx.moveTo(fx, fy);
ctx.lineTo(tx, ty);
if (isOffline) {
ctx.strokeStyle = 'rgba(239, 68, 68, 0.15)';
ctx.setLineDash([4, 6]);
} else {
ctx.strokeStyle = 'rgba(6, 182, 212, 0.15)';
ctx.setLineDash([]);
}
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.setLineDash([]);
});
// Draw and update packets
packets = packets.filter(p => {
p.progress += p.speed;
if (p.progress >= 1) return false;
const from = networkNodes.find(n => n.id === p.fromId);
const to = networkNodes.find(n => n.id === p.toId);
if (!from || !to) return false;
const fx = from.x * w;
const fy = from.y * h;
const tx = to.x * w;
const ty = to.y * h;
const px = fx + (tx - fx) * p.progress;
const py = fy + (ty - fy) * p.progress;
// Glow
ctx.beginPath();
ctx.arc(px, py, 5, 0, Math.PI * 2);
ctx.fillStyle = p.color + '30';
ctx.fill();
// Core
ctx.beginPath();
ctx.arc(px, py, 2.5, 0, Math.PI * 2);
ctx.fillStyle = p.color;
ctx.fill();
return true;
});
// Draw nodes
networkNodes.forEach(node => {
const nx = node.x * w;
const ny = node.y * h;
const isHovered = hoveredNode === node;
const radius = isHovered ? 18 : 14;
// Glow for online nodes
if (node.status === 'online') {
const grad = ctx.createRadialGradient(nx, ny, 0, nx, ny, radius + 12);
grad.addColorStop(0, 'rgba(34, 197, 94, 0.3)');
grad.addColorStop(1, 'rgba(34, 197, 94, 0)');
ctx.beginPath();
ctx.arc(nx, ny, radius + 12, 0, Math.PI * 2);
ctx.fillStyle = grad;
ctx.fill();
} else if (node.status === 'warning') {
const grad = ctx.createRadialGradient(nx, ny, 0, nx, ny, radius + 12);
grad.addColorStop(0, 'rgba(245, 158, 11, 0.3)');
grad.addColorStop(1, 'rgba(245, 158, 11, 0)');
ctx.beginPath();
ctx.arc(nx, ny, radius + 12, 0, Math.PI * 2);
ctx.fillStyle = grad;
ctx.fill();
}
// Node circle
ctx.beginPath();
ctx.arc(nx, ny, radius, 0, Math.PI * 2);
if (node.status === 'online') {
ctx.fillStyle = '#1e293b';
ctx.strokeStyle = '#22c55e';
} else if (node.status === 'warning') {
ctx.fillStyle = '#1e293b';
ctx.strokeStyle = '#f59e0b';
} else {
ctx.fillStyle = '#0f172a';
ctx.strokeStyle = '#ef4444';
}
ctx.lineWidth = 2;
ctx.fill();
ctx.stroke();
// Status dot
const dotColor = node.status === 'online' ? '#22c55e' : node.status === 'warning' ? '#f59e0b' : '#ef4444';
ctx.beginPath();
ctx.arc(nx, ny, 4, 0, Math.PI * 2);
ctx.fillStyle = dotColor;
ctx.fill();
// Node label
ctx.fillStyle = '#cbd5e1';
ctx.font = '11px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText(node.name, nx, ny + radius + 6);
// IP address (smaller, below name)
ctx.fillStyle = '#475569';
ctx.font = '9px JetBrains Mono, monospace';
ctx.fillText(node.ip, nx, ny + radius + 20);
});
// Update packets per sec counter
packetsPerSec = packets.length;
const ppsEl = document.getElementById('packetsPerSec');
if (ppsEl) ppsEl.textContent = packetsPerSec;
topologyAnimId = requestAnimationFrame(animateTopology);
}
// ---- Refresh Topology ----
function refreshTopology() {
// Randomly toggle a node status for visual feedback
const randomNode = networkNodes[Math.floor(Math.random() * networkNodes.length)];
if (randomNode && randomNode.id !== 'internet') {
const statuses = ['online', 'online', 'online', 'warning', 'offline'];
randomNode.status = statuses[Math.floor(Math.random() * statuses.length)];
}
renderNodeStatus();
showToast('Topology refreshed');
}
// ---- Node Status List ----
function renderNodeStatus() {
const list = document.getElementById('nodeStatusList');
if (!list) return;
const statusColors = {
online: 'text-green-400 bg-green-500/10 border-green-500/20',
warning: 'text-amber-400 bg-amber-500/10 border-amber-500/20',
offline: 'text-red-400 bg-red-500/10 border-red-500/20',
};
const statusIcons = {
online: 'check-circle',
warning: 'alert-triangle',
offline: 'x-circle',
};
list.innerHTML = networkNodes.map((n, i) => {
const sc = statusColors[n.status] || statusColors.offline;
const [textC, bgC, borderC] = sc.split(' ');
return `
${n.name}
${n.status}
${n.ip} · ${n.type}
`;
}).join('');
lucide.createIcons();
// Update node status count
const online = networkNodes.filter(n => n.status === 'online').length;
const total = networkNodes.length;
const countEl = document.getElementById('nodeStatusCount');
if (countEl) countEl.textContent = `${online}/${total}`;
const onlineEl = document.getElementById('nodesOnline');
if (onlineEl) onlineEl.textContent = online;
}
// ---- Active Connections ----
function initConnections() {
for (let i = 0; i < 8; i++) {
addConnection();
}
setInterval(addConnection, 5000);
}
function addConnection() {
const template = connTemplates[Math.floor(Math.random() * connTemplates.length)];
const extIP = extIPs[Math.floor(Math.random() * extIPs.length)];
const internalNode = networkNodes[Math.floor(Math.random() * (networkNodes.length - 1))];
const ports = [22, 80, 443, 3306, 5432, 8080, 8443, 9090];
const port = ports[Math.floor(Math.random() * ports.length)];
const now = new Date();
const timeStr = now.toTimeString().split(' ')[0];
const conn = {
id: Math.random().toString(36).substring(7),
source: extIP,
dest: internalNode ? internalNode.ip : '10.0.0.1',
destName: internalNode ? internalNode.name : 'unknown',
port: port,
proto: template.proto,
state: template.state,
time: timeStr,
};
activeConnections.unshift(conn);
if (activeConnections.length > 15) activeConnections.pop();
renderConnections();
}
function renderConnections() {
const list = document.getElementById('connectionsList');
if (!list) return;
const stateColors = {
ESTABLISHED: 'text-green-400 bg-green-500/10',
TIME_WAIT: 'text-amber-400 bg-amber-500/10',
CLOSE_WAIT: 'text-red-400 bg-red-500/10',
};
list.innerHTML = activeConnections.map(c => {
const sc = stateColors[c.state] || 'text-slate-400 bg-slate-500/10';
const [textC, bgC] = sc.split(' ');
return `
${c.source}
${c.dest}
:${c.port}
${c.proto}
${c.state}
${c.time}
`;
}).join('');
lucide.createIcons();
}
// ---- Port Scan Table ----
function renderPortScan() {
const tbody = document.getElementById('portScanBody');
if (!tbody) return;
const stateColors = {
open: 'text-green-400 bg-green-500/10 border-green-500/20',
filtered: 'text-amber-400 bg-amber-500/10 border-amber-500/20',
closed: 'text-red-400 bg-red-500/10 border-red-500/20',
};
tbody.innerHTML = portScanData.map(p => {
const sc = stateColors[p.state] || stateColors.closed;
const [textC, bgC, borderC] = sc.split(' ');
return `
| ${p.asset} |
${p.ip} |
${p.port} |
${p.service} |
${p.proto} |
${p.state}
|
${p.banner} |
`;
}).join('');
lucide.createIcons();
}
// ---- Real Monitoring via Public APIs ----
async function startRealMonitoring() {
// 1. Resolve real domain via Google DNS
const dnsData = await SentinelAPI.resolveDomain('google.com');
if (dnsData && dnsData.ips && dnsData.ips.length > 0) {
extIPs = dnsData.ips.slice(0, 8);
showToast(`Resolved ${dnsData.ips.length} real IPs via Google DNS`);
} else {
// Fallback to static IPs if DNS fails
extIPs = ['203.0.113.45', '198.51.100.23', '192.0.2.99', '185.220.101.45', '45.155.205.99', '91.134.243.12', '172.16.254.1', '203.0.113.77'];
}
// 2. Lookup real IP via ipinfo.io
const ipData = await SentinelAPI.lookupIP('8.8.8.8');
if (ipData && !ipData.error) {
const realNode = networkNodes.find(n => n.id === 'internet');
if (realNode) {
realNode.name = ipData.org ? ipData.org.split(' ')[0] : 'Internet';
realNode.ip = ipData.ip || '8.8.8.8';
}
showToast(`Real IP: ${ipData.ip} — ${ipData.city}, ${ipData.country}`);
}
// 3. Update connections list with real IPs periodically
setInterval(async () => {
if (extIPs.length === 0) return;
const randomIP = extIPs[Math.floor(Math.random() * extIPs.length)];
const resolved = await SentinelAPI.lookupIP(randomIP);
if (resolved && !resolved.error && resolved.ip) {
// Update network node with real data
const internetNode = networkNodes.find(n => n.id === 'internet');
if (internetNode) {
internetNode.ip = resolved.ip;
}
renderNodeStatus();
}
}, 15000);
// 4. Keep visual metrics cycling (no public API for local bandwidth)
setInterval(() => {
const bw = (Math.random() * 5 + 8).toFixed(1);
const bwEl = document.getElementById('bandwidthVal');
if (bwEl) bwEl.textContent = bw;
}, 3000);
setInterval(() => {
const latency = Math.floor(Math.random() * 10 + 12);
const latEl = document.getElementById('latencyVal');
if (latEl) latEl.textContent = latency;
}, 2500);
setInterval(() => {
const conns = Math.floor(Math.random() * 50 + 180);
const connEl = document.getElementById('connCount');
if (connEl) connEl.textContent = conns;
}, 4000);
// 5. Network node health simulation (realistic fluctuations)
setInterval(() => {
const randomNode = networkNodes[Math.floor(Math.random() * networkNodes.length)];
if (randomNode && randomNode.id !== 'internet' && randomNode.id !== 'router' && randomNode.id !== 'firewall') {
const roll = Math.random();
if (roll < 0.02) {
randomNode.status = 'offline';
} else if (roll < 0.05) {
randomNode.status = 'warning';
} else {
randomNode.status = 'online';
}
renderNodeStatus();
}
}, 10000);
renderNodeStatus();
showToast('Network monitoring: REAL operational mode active');
}
// ---- Mobile Menu ----
function toggleMobileMenu() {
const menu = document.getElementById('mobileMenu');
if (menu) menu.classList.toggle('hidden');
}
// ---- Toast ----
function showToast(message) {
const toast = document.getElementById('netToast');
const msgEl = document.getElementById('netToastMsg');
if (!toast || !msgEl) return;
msgEl.textContent = message;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 2500);
}