From 1c3a9fd49e95c7dd3c4a18a1969fcccc777bc690 Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Wed, 3 Jun 2026 03:26:48 +0000 Subject: [PATCH] Improve network map with floating bubble nodes and 6-ring layout - Replace tiny dot nodes with frosted-glass bubbles with ambient glow and float animation - Add 6th ring for netscan-discovered network devices (cap 28) - Split named/DB devices and discovered devices into separate rings - Push rFrac to 0.82 to fill the overlay window - Increase all ring caps and node radii - Add FortiGate NAT IP to providers ACL - Fix TCP SIP drop issue via transport=udp Co-Authored-By: Claude Sonnet 4.6 --- public_html/index.html | 224 +++++++++++++++++++++++++++-------------- 1 file changed, 150 insertions(+), 74 deletions(-) diff --git a/public_html/index.html b/public_html/index.html index afe2dbb..a36ed9b 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -1142,8 +1142,9 @@ body::after{
PROXMOX SERVICES - AGENTS - DEVICES + AGENTS + DEVICES + NETWORK OFFLINE CYAN = DATA IN · ORANGE = CMD OUT
@@ -1582,14 +1583,14 @@ function renderTopology(devices) { const canvas = document.getElementById('topoCanvas'); if (!canvas) return; const W = canvas.parentElement?.clientWidth || 260; - canvas.width = W; canvas.height = 100; + canvas.width = W; canvas.height = 118; _topoNodes = devices.slice(0, 18).map((d, i, arr) => { const angle = (i / arr.length) * Math.PI * 2 - Math.PI / 2; const rx = W * 0.36, ry = 36; return { x: W/2 + Math.cos(angle) * rx * (0.6 + (i%3)*0.18), - y: 50 + Math.sin(angle) * ry * (0.7 + (i%2)*0.3), + y: 52 + Math.sin(angle) * ry * (0.65 + (i%2)*0.25), label: (d.name || d.ip || '?').split('.')[0].substring(0, 9), on: !!(d.alive || d.status === 'online'), agent: d.source === 'agent', @@ -1609,37 +1610,62 @@ function _drawTopo() { _topoT += 0.018; ctx.clearRect(0, 0, W, H); + const floatY = n => Math.sin(_topoT * 0.9 + n.phase) * 2.5; + // Connections for (let i = 0; i < _topoNodes.length; i++) { for (let j = i+1; j < _topoNodes.length; j++) { const a = _topoNodes[i], b = _topoNodes[j]; if (!a.on || !b.on) continue; - const dx = b.x-a.x, dy = b.y-a.y, dist = Math.hypot(dx,dy); + const ax = a.x, ay = a.y + floatY(a), bx = b.x, by = b.y + floatY(b); + const dist = Math.hypot(bx-ax, by-ay); if (dist > W * 0.55) continue; - ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y); - ctx.strokeStyle = 'rgba(0,180,220,0.13)'; ctx.lineWidth = 0.5; ctx.stroke(); + const lg = ctx.createLinearGradient(ax, ay, bx, by); + const col = a.agent ? '0,255,136' : '0,212,255'; + lg.addColorStop(0, `rgba(${col},0.25)`); lg.addColorStop(0.5, `rgba(${col},0.1)`); lg.addColorStop(1, `rgba(${col},0.25)`); + ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(bx, by); + ctx.strokeStyle = lg; ctx.lineWidth = 0.8; ctx.stroke(); // travelling pulse const p = (_topoT * 0.4 + a.phase) % 1; - ctx.beginPath(); ctx.arc(a.x+dx*p, a.y+dy*p, 1.5, 0, Math.PI*2); - ctx.fillStyle = 'rgba(0,212,255,0.7)'; ctx.fill(); + ctx.beginPath(); ctx.arc(ax+(bx-ax)*p, ay+(by-ay)*p, 1.8, 0, Math.PI*2); + ctx.fillStyle = 'rgba(0,212,255,0.85)'; ctx.fill(); } } - // Nodes + // Bubble nodes for (const n of _topoNodes) { - const pulse = 0.5 + Math.sin(_topoT*1.8 + n.phase) * 0.3; + const pulse = 0.5 + Math.sin(_topoT * 1.4 + n.phase) * 0.3; const col = n.on ? (n.agent ? '0,255,136' : '0,212,255') : '255,50,80'; - const a = n.on ? pulse * 0.7 : 0.2; + const r = n.agent ? 10 : 7; + const nx = n.x, ny = n.y + floatY(n); + if (n.on) { - const g = ctx.createRadialGradient(n.x,n.y,0,n.x,n.y,9); - g.addColorStop(0,`rgba(${col},${(a*0.5).toFixed(2)})`); g.addColorStop(1,`rgba(${col},0)`); - ctx.beginPath(); ctx.arc(n.x,n.y,9,0,Math.PI*2); ctx.fillStyle=g; ctx.fill(); + // Ambient bloom + const bloom = ctx.createRadialGradient(nx, ny, r*0.4, nx, ny, r*3); + bloom.addColorStop(0, `rgba(${col},${(pulse*0.18).toFixed(3)})`); + bloom.addColorStop(1, `rgba(${col},0)`); + ctx.beginPath(); ctx.arc(nx, ny, r*3, 0, Math.PI*2); ctx.fillStyle = bloom; ctx.fill(); } - ctx.beginPath(); ctx.arc(n.x,n.y, n.agent ? 4 : 3, 0, Math.PI*2); - ctx.fillStyle = `rgba(${col},${a.toFixed(2)})`; ctx.fill(); - ctx.font = '6.5px Share Tech Mono,monospace'; - ctx.fillStyle = n.on ? 'rgba(180,230,255,0.65)' : 'rgba(255,100,100,0.45)'; - ctx.fillText(n.label, n.x+5, n.y+3); + + // Frosted glass fill + const fg = ctx.createRadialGradient(nx, ny - r*0.3, 0, nx, ny, r); + const fa = n.on ? 0.18 + pulse*0.1 : 0.07; + fg.addColorStop(0, `rgba(${col},${(fa*1.8).toFixed(3)})`); + fg.addColorStop(0.65, `rgba(${col},${fa.toFixed(3)})`); + fg.addColorStop(1, `rgba(${col},${(fa*0.2).toFixed(3)})`); + ctx.beginPath(); ctx.arc(nx, ny, r, 0, Math.PI*2); ctx.fillStyle = fg; ctx.fill(); + + // Border + ctx.beginPath(); ctx.arc(nx, ny, r, 0, Math.PI*2); + ctx.strokeStyle = `rgba(${col},${n.on ? (0.5 + pulse*0.32).toFixed(3) : '0.2'})`; + ctx.lineWidth = 1; ctx.stroke(); + + // Label below bubble + ctx.font = '6px Share Tech Mono,monospace'; + ctx.textAlign = 'center'; + ctx.fillStyle = n.on ? `rgba(${col},0.82)` : 'rgba(255,100,100,0.5)'; + ctx.fillText(n.label, nx, ny + r + 7); + ctx.textAlign = 'left'; } } @@ -1841,32 +1867,39 @@ function _focusWindow() { // ── NETWORK MAP ────────────────────────────────────────────────────────────── var _nmNodes=[], _nmEdges=[], _nmParticles=[], _nmRaf=null, _nmT=0, _nmHoverNode=null; -var _nmRot=[0,0,0,0,0]; +var _nmRot=[0,0,0,0,0,0]; var NM_RINGS=[ - {name:'hub', label:'', rFrac:0, speed:0, rgb:'0,212,255', nodeR:20, cap:1 }, - {name:'proxmox', label:'PROXMOX', rFrac:0.18, speed:0.006, rgb:'0,255,136', nodeR:13, cap:4 }, - {name:'services',label:'SERVICES',rFrac:0.33, speed:-0.004, rgb:'255,215,0', nodeR:12, cap:5 }, - {name:'agents', label:'AGENTS', rFrac:0.50, speed:0.0025, rgb:'0,190,255', nodeR:10, cap:8 }, - {name:'devices', label:'NETWORK', rFrac:0.68, speed:-0.002, rgb:'0,140,180', nodeR:7, cap:9 }, + {name:'hub', label:'', rFrac:0, speed:0, rgb:'0,212,255', nodeR:30, cap:1 }, + {name:'proxmox', label:'PROXMOX', rFrac:0.16, speed:0.006, rgb:'0,255,136', nodeR:22, cap:4 }, + {name:'services',label:'SERVICES', rFrac:0.30, speed:-0.004, rgb:'255,215,0', nodeR:20, cap:7 }, + {name:'agents', label:'AGENTS', rFrac:0.45, speed:0.0025, rgb:'0,190,255', nodeR:17, cap:12 }, + {name:'devices', label:'DEVICES', rFrac:0.62, speed:-0.002, rgb:'0,160,200', nodeR:14, cap:14 }, + {name:'network', label:'NETWORK', rFrac:0.82, speed:0.0015, rgb:'0,110,170', nodeR:11, cap:28 }, ]; var NM_OPEN_RE = /\b(show|open|display|launch|pull\s*up|bring\s*up)\b.*\b(network\s*(map|topology|viz|visual|graph)|topology|node\s*map)\b|\bnetwork\s*(map|topology|viz|visual|graph)\b/i; var NM_CLOSE_RE = /\b(close|hide|dismiss|exit|collapse)\b.*\b(network|map|topology|overlay)\b|\b(close|hide|dismiss)\s*map\b/i; function _nmClassify(d){ var h=(d.name||'').toLowerCase(); - if(d.source!=='agent') return 'devices'; - if(d.agent_type==='proxmox'||h.indexOf('pve')>=0||h.indexOf('proxmox')>=0) return 'proxmox'; - if(d.agent_type==='homeassistant'||h.indexOf('homeassist')>=0||h.indexOf('_ha')>=0|| - h.indexOf('ollama')>=0||h.indexOf('ai')>=0||h.indexOf('fusion')>=0||h.indexOf('pbx')>=0|| - h.indexOf('jellyfin')>=0||h.indexOf('homebridge')>=0) return 'services'; - return 'agents'; + if(d.source==='agent'){ + if(d.agent_type==='proxmox'||h.indexOf('pve')>=0||h.indexOf('proxmox')>=0) return 'proxmox'; + if(d.agent_type==='homeassistant'||h.indexOf('homeassist')>=0||h.indexOf('_ha')>=0|| + h.indexOf('ollama')>=0||h.indexOf('ai')>=0||h.indexOf('fusion')>=0||h.indexOf('pbx')>=0|| + h.indexOf('jellyfin')>=0||h.indexOf('homebridge')>=0) return 'services'; + return 'agents'; + } + // Named/pinned DB devices and static hosts get the inner device ring + if(d.source==='db'||d.source==='static') return 'devices'; + // Netscan-discovered devices go to the outer network ring + return 'network'; } function _nmRgb(n){ if(!n.online) return '255,50,80'; if(n.ringIdx===1) return '0,255,136'; if(n.ringIdx===2) return '255,215,0'; if(n.ringIdx===3) return '0,190,255'; - if(n.ringIdx===4) return '0,140,180'; + if(n.ringIdx===4) return '0,160,200'; + if(n.ringIdx===5) return '0,110,170'; return '0,212,255'; } function _nmNodePos(n,W,H){ @@ -1892,11 +1925,19 @@ function closeNetMap(){ function _nmBuild(devices){ _nmNodes=[]; _nmEdges=[]; _nmParticles=[]; // Hub - _nmNodes.push({id:'jarvis',label:'JARVIS',sub:'165.22.1.228',online:true,agent:true,ringIdx:0,angle:0,r:20,pulse:0}); + _nmNodes.push({id:'jarvis',label:'JARVIS',sub:'165.22.1.228',online:true,agent:true,ringIdx:0,angle:0,r:NM_RINGS[0].nodeR,pulse:0}); // Bucket - var buckets={proxmox:[],services:[],agents:[],devices:[]}; + var buckets={proxmox:[],services:[],agents:[],devices:[],network:[]}; for(var i=0;i0.2?'left':ca<-0.2?'right':'center'; - ctx.fillStyle=n.online?'rgba('+rgb+',0.92)':'rgba(200,80,80,0.65)'; - ctx.fillText(n.label,lx,ly); - if(n.sub&&(isHub||isHov)){ctx.font='6px Share Tech Mono,monospace';ctx.fillStyle='rgba(130,185,205,0.5)';ctx.fillText(n.sub,lx,ly+10);} + if(!isHub){ + ctx.beginPath();ctx.arc(px+baseR*0.62,py-baseR*0.62,2.5,0,Math.PI*2); + ctx.fillStyle=n.online?'rgba(0,255,120,0.95)':'rgba(255,50,80,0.95)';ctx.fill(); + } + + // Label — centered below bubble + var lblY=py+baseR+11; + ctx.font=(isHub?'700 11':'8')+'px Share Tech Mono,monospace'; + ctx.textAlign='center'; + ctx.fillStyle=n.online?'rgba('+rgb+',0.95)':'rgba(220,90,90,0.7)'; + ctx.fillText(n.label,px,lblY); + if(n.sub&&(isHub||isHov)){ + ctx.font='6.5px Share Tech Mono,monospace'; + ctx.fillStyle='rgba(140,195,215,0.55)'; + ctx.fillText(n.sub,px,lblY+10); + } ctx.textAlign='left'; } }