| |
| |
| |
|
|
| |
| 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' }, |
| ]; |
|
|
| |
| 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' }, |
| ]; |
|
|
| |
| 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' }, |
| ]; |
|
|
| |
| 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 = []; |
|
|
| |
| 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; |
|
|
| |
| document.addEventListener('DOMContentLoaded', async () => { |
| lucide.createIcons(); |
| initStats(); |
| initTrafficData(); |
| initConnections(); |
| renderNodeStatus(); |
| renderPortScan(); |
| initTopology(); |
| initTrafficChart(); |
| startClock(); |
| await startRealMonitoring(); |
| }); |
|
|
| |
| 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); |
| } |
|
|
| |
| 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); |
| } |
|
|
| |
| 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); |
| } |
|
|
| |
| 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`; |
| } |
|
|
| |
| 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); |
|
|
| |
| 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(); |
| } |
|
|
| |
| 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(); |
|
|
| |
| 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(); |
|
|
| |
| 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(); |
|
|
| |
| 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(); |
| } |
|
|
| |
| 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); |
|
|
| |
| 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 = ` |
| <div class="text-white font-semibold">${hoveredNode.name}</div> |
| <div class="text-slate-400">${hoveredNode.ip}</div> |
| <div class="${statusColor} uppercase font-bold mt-0.5">${hoveredNode.status}</div> |
| <div class="text-slate-500 capitalize">${hoveredNode.type}</div> |
| `; |
| } |
| } 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'); |
| }); |
|
|
| |
| 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); |
|
|
| |
| 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([]); |
| }); |
|
|
| |
| 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; |
|
|
| |
| ctx.beginPath(); |
| ctx.arc(px, py, 5, 0, Math.PI * 2); |
| ctx.fillStyle = p.color + '30'; |
| ctx.fill(); |
|
|
| |
| ctx.beginPath(); |
| ctx.arc(px, py, 2.5, 0, Math.PI * 2); |
| ctx.fillStyle = p.color; |
| ctx.fill(); |
|
|
| return true; |
| }); |
|
|
| |
| networkNodes.forEach(node => { |
| const nx = node.x * w; |
| const ny = node.y * h; |
| const isHovered = hoveredNode === node; |
| const radius = isHovered ? 18 : 14; |
|
|
| |
| 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(); |
| } |
|
|
| |
| 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(); |
|
|
| |
| 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(); |
|
|
| |
| ctx.fillStyle = '#cbd5e1'; |
| ctx.font = '11px Inter, sans-serif'; |
| ctx.textAlign = 'center'; |
| ctx.textBaseline = 'top'; |
| ctx.fillText(node.name, nx, ny + radius + 6); |
|
|
| |
| ctx.fillStyle = '#475569'; |
| ctx.font = '9px JetBrains Mono, monospace'; |
| ctx.fillText(node.ip, nx, ny + radius + 20); |
| }); |
|
|
| |
| packetsPerSec = packets.length; |
| const ppsEl = document.getElementById('packetsPerSec'); |
| if (ppsEl) ppsEl.textContent = packetsPerSec; |
|
|
| topologyAnimId = requestAnimationFrame(animateTopology); |
| } |
|
|
| |
| function refreshTopology() { |
| |
| 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'); |
| } |
|
|
| |
| 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 ` |
| <div class="node-status-item flex items-center gap-3 p-2.5 rounded-lg" style="animation-delay: ${i * 40}ms"> |
| <div class="w-8 h-8 ${bgC} rounded-md flex items-center justify-center shrink-0 border ${borderC}"> |
| <i data-lucide="${statusIcons[n.status] || 'circle'}" class="w-4 h-4 ${textC}"></i> |
| </div> |
| <div class="flex-1 min-w-0"> |
| <div class="flex items-center gap-2"> |
| <span class="text-xs font-medium text-slate-200 truncate">${n.name}</span> |
| <span class="text-[10px] font-mono ${textC} uppercase font-bold">${n.status}</span> |
| </div> |
| <span class="text-[10px] font-mono text-slate-600">${n.ip} · ${n.type}</span> |
| </div> |
| </div> |
| `; |
| }).join(''); |
|
|
| lucide.createIcons(); |
|
|
| |
| 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; |
| } |
|
|
| |
| 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 ` |
| <div class="conn-item flex items-center gap-2 p-2.5 rounded-lg"> |
| <div class="flex-1 min-w-0"> |
| <div class="flex items-center gap-1.5 text-xs"> |
| <span class="font-mono text-slate-300 truncate">${c.source}</span> |
| <i data-lucide="arrow-right" class="w-3 h-3 text-slate-600 shrink-0"></i> |
| <span class="font-mono text-cyan-400 truncate">${c.dest}</span> |
| </div> |
| <div class="flex items-center gap-2 mt-1"> |
| <span class="text-[10px] font-mono text-slate-600">:${c.port}</span> |
| <span class="text-[10px] font-mono text-slate-600">${c.proto}</span> |
| <span class="text-[10px] font-mono ${textC} px-1.5 py-0.5 rounded ${bgC} font-semibold">${c.state}</span> |
| </div> |
| </div> |
| <span class="text-[10px] font-mono text-slate-600 shrink-0">${c.time}</span> |
| </div> |
| `; |
| }).join(''); |
|
|
| lucide.createIcons(); |
| } |
|
|
| |
| 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 ` |
| <tr> |
| <td class="px-4 py-3 text-sm text-slate-200 font-medium">${p.asset}</td> |
| <td class="px-4 py-3 font-mono text-xs text-slate-400">${p.ip}</td> |
| <td class="px-4 py-3 font-mono text-xs text-cyan-400">${p.port}</td> |
| <td class="px-4 py-3 text-sm text-slate-300">${p.service}</td> |
| <td class="px-4 py-3 font-mono text-xs text-slate-500">${p.proto}</td> |
| <td class="px-4 py-3"> |
| <span class="${textC} ${bgC} border ${borderC} px-2 py-1 rounded text-xs font-semibold uppercase">${p.state}</span> |
| </td> |
| <td class="px-4 py-3 font-mono text-xs text-slate-500 hidden sm:table-cell">${p.banner}</td> |
| </tr> |
| `; |
| }).join(''); |
|
|
| lucide.createIcons(); |
| } |
|
|
| |
| async function startRealMonitoring() { |
| |
| 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 { |
| |
| 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']; |
| } |
|
|
| |
| 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}`); |
| } |
|
|
| |
| 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) { |
| |
| const internetNode = networkNodes.find(n => n.id === 'internet'); |
| if (internetNode) { |
| internetNode.ip = resolved.ip; |
| } |
| renderNodeStatus(); |
| } |
| }, 15000); |
|
|
| |
| 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); |
|
|
| |
| 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'); |
| } |
|
|
| |
| function toggleMobileMenu() { |
| const menu = document.getElementById('mobileMenu'); |
| if (menu) menu.classList.toggle('hidden'); |
| } |
|
|
| |
| 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); |
| } |