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';
}
}