@@ -1743,306 +1712,230 @@ function _drawTopo() {
tick();
})();
-// ── NETWORK MAP OVERLAY ───────────────────────────────────────────────
-let _nmNodes = [], _nmEdges = [], _nmParticles = [], _nmRaf = null, _nmT = 0;
-let _nmHoverNode = null;
-const NM_OPEN_RE = /\b(show|open|display|launch|pull\s*up|bring\s*up)\b.*\b(network\s*(map|topology|viz|visual|status|graph|diagram)|topology|node\s*map|connection\s*map)\b|\b(network\s*(map|topology|viz|visual|graph|diagram))\b/i;
-const NM_CLOSE_RE = /\b(close|hide|dismiss|exit|shut|collapse)\b.*\b(network|map|topology|overlay|viz)\b|\b(close\s*map|hide\s*map|dismiss\s*map)\b/i;
-async function openNetMap() {
- const ov = document.getElementById('netMapOverlay');
- if (!ov) return;
- ov.classList.remove('nm-closing');
- ov.classList.add('nm-open');
-
- // Fetch network data
- let devices = [];
- try { const n = await api('network'); devices = n.devices || []; } catch(_) {}
-
- _buildNetGraph(devices);
- _startNetDraw();
-}
-
-function closeNetMap() {
- const ov = document.getElementById('netMapOverlay');
- if (!ov) return;
- ov.classList.add('nm-closing');
- setTimeout(() => { ov.classList.remove('nm-open','nm-closing'); }, 350);
- if (_nmRaf) { cancelAnimationFrame(_nmRaf); _nmRaf = null; }
-}
-
-
-// Orbital ring definitions — each ring has its own radius ratio, speed, color, node cap
-const 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.34, speed:-0.004, rgb:'255,215,0', nodeR:12, cap:5 },
- { name:'agents', label:'AGENTS', rFrac:0.52, speed: 0.0025, rgb:'0,190,255', nodeR:10, cap:8 },
- { name:'devices', label:'NETWORK', rFrac:0.70, speed:-0.0015, rgb:'0,140,180', nodeR: 7, cap:10 },
+// ── NETWORK MAP ──────────────────────────────────────────────────────────────
+var _nmNodes=[], _nmEdges=[], _nmParticles=[], _nmRaf=null, _nmT=0, _nmHoverNode=null;
+var _nmRot=[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 },
];
+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 _classifyDevice(d) {
- const h = (d.name||'').toLowerCase();
- if (d.source !== 'agent') return 'devices';
- if (d.agent_type==='proxmox' || h.includes('pve') || h.includes('proxmox')) return 'proxmox';
- if (d.agent_type==='homeassistant' || h.includes('homeassist') || h.includes('_ha') ||
- h.includes('ollama') || h.includes('ai') || h.includes('fusion') || h.includes('pbx') ||
- h.includes('jellyfin') || h.includes('homebridge')) return 'services';
+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';
}
-
-function _nodeRgb(n) {
- if (n.ring === 'hub') return '0,212,255';
- if (n.ring === 'proxmox') return '0,255,136';
- if (n.ring === 'services') return '255,215,0';
- if (n.ring === 'agents') return '0,190,255';
- if (!n.online) return '255,50,80';
- return '0,140,180';
+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';
+ return '0,212,255';
+}
+function _nmNodePos(n,W,H){
+ if(n.ringIdx===0) return {x:W/2, y:H/2};
+ var rd=NM_RINGS[n.ringIdx], rot=_nmRot[n.ringIdx]||0;
+ var r=Math.min(W/2,H/2)*rd.rFrac;
+ return {x:W/2+Math.cos(n.angle+rot)*r, y:H/2+Math.sin(n.angle+rot)*r};
}
-// Ring rotation offsets (persist across redraws)
-const _nmRot = [0, 0, 0, 0, 0];
-
-function _buildNetGraph(devices) {
- const ov = document.getElementById('netMapOverlay');
- const W = ov ? ov.clientWidth : 860;
- const H = (ov ? ov.clientHeight : 570) - 70; // minus header+legend
- const cx = W / 2, cy = H / 2;
- _nmNodes = []; _nmEdges = []; _nmParticles = [];
- const cx = W/2, cy = H/2, minDim = Math.min(cx, cy);
-
- // Hub at center
- _nmNodes.push({ id:'jarvis', label:'JARVIS', sub:'165.22.1.228',
- type:'hub', online:true, agent:true, ring:'hub', angle:0, r:20, pulse:0, ringIdx:0 });
-
- // Bucket into rings
- const buckets = { proxmox:[], services:[], agents:[], devices:[] };
- devices.forEach(d => buckets[_classifyDevice(d)].push(d));
-
- ['proxmox','services','agents','devices'].forEach((ringName, ri) => {
- const rd = NM_RINGS[ri + 1];
- const list = buckets[ringName].slice(0, rd.cap);
- list.forEach((d, i) => {
- const baseAngle = (ri % 2 === 0 ? -Math.PI/2 : -Math.PI/3);
- const angle = baseAngle + (i / Math.max(list.length, 1)) * Math.PI * 2;
+async function openNetMap(){
+ var ov=document.getElementById('netMapOverlay'); if(!ov) return;
+ ov.classList.remove('nm-closing'); ov.classList.add('nm-open');
+ var devices=[];
+ try{ var n=await api('network'); devices=n.devices||[]; }catch(e){}
+ _nmBuild(devices); _nmDraw();
+}
+function closeNetMap(){
+ var ov=document.getElementById('netMapOverlay'); if(!ov) return;
+ ov.classList.add('nm-closing');
+ setTimeout(function(){ ov.classList.remove('nm-open','nm-closing'); }, 350);
+ if(_nmRaf){ cancelAnimationFrame(_nmRaf); _nmRaf=null; }
+}
+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});
+ // Bucket
+ var buckets={proxmox:[],services:[],agents:[],devices:[]};
+ for(var i=0;i0.45) _nmParticles.push({edge:_nmEdges.length-1,t:Math.random(),dir:'out',speed:0.0013+Math.random()*0.0017,r:1.5+Math.random()*0.7});
+ }
}
-
- // Particles
- _nmEdges.forEach((e, ei) => {
- const n = _nmNodes[e.from];
- const cnt = n.online ? (n.agent ? 3 : 2) : 0;
- for (let p = 0; p < cnt; p++)
- _nmParticles.push({ edge:ei, t:Math.random(), dir:'in',
- speed:0.002+Math.random()*0.0025, r:1.8+Math.random()*0.9 });
- if (n.online && n.agent && Math.random()>0.45)
- _nmParticles.push({ edge:ei, t:Math.random(), dir:'out',
- speed:0.0013+Math.random()*0.0017, r:1.5+Math.random()*0.7 });
- });
-
// Stats
- const online = _nmNodes.filter(n=>n.online).length;
- const agt = _nmNodes.filter(n=>n.agent).length;
- const g = id => document.getElementById(id);
- if(g('nm-node-count')) g('nm-node-count').textContent = _nmNodes.length;
- if(g('nm-online-count')) g('nm-online-count').textContent = online;
- if(g('nm-agent-count')) g('nm-agent-count').textContent = agt;
+ var online=_nmNodes.filter(function(n){return n.online;}).length;
+ var agt=_nmNodes.filter(function(n){return n.agent;}).length;
+ function sg(id){return document.getElementById(id);}
+ if(sg('nm-node-count')) sg('nm-node-count').textContent=_nmNodes.length;
+ if(sg('nm-online-count')) sg('nm-online-count').textContent=online;
+ if(sg('nm-agent-count')) sg('nm-agent-count').textContent=agt;
}
+function _nmDraw(){
+ if(_nmRaf) cancelAnimationFrame(_nmRaf);
+ var canvas=document.getElementById('nmCanvas'); if(!canvas) return;
+ var ov=document.getElementById('netMapOverlay');
+ canvas.width = ov ? ov.clientWidth : 820;
+ canvas.height= ov ? ov.clientHeight-62 : 478;
+ var W=canvas.width, H=canvas.height, cx=W/2, cy=H/2, minR=Math.min(cx,cy);
+ var ctx=canvas.getContext('2d');
-function _startNetDraw() {
- if (_nmRaf) cancelAnimationFrame(_nmRaf);
- const canvas = document.getElementById('nmCanvas');
- if (!canvas) return;
- const ov = document.getElementById('netMapOverlay');
- canvas.width = ov ? ov.clientWidth : 860;
- canvas.height = (ov ? ov.clientHeight : 570) - 72;
- const W = canvas.width, H = canvas.height;
- const cx = W/2, cy = H/2, minDim = Math.min(cx, cy);
+ function frame(){
+ if(!document.getElementById('netMapOverlay').classList.contains('nm-open')) return;
+ _nmRaf=requestAnimationFrame(frame);
+ _nmT+=0.016;
+ for(var i=0;i { _nmRot[i] = (_nmRot[i]||0) + rd.speed; });
-
- const ctx = canvas.getContext('2d');
- ctx.clearRect(0, 0, W, H);
-
- // Subtle dot grid
- ctx.fillStyle = 'rgba(0,180,255,0.055)';
- for (let x = 26; x < W; x += 40) for (let y = 26; y < H; y += 40) {
- ctx.beginPath(); ctx.arc(x,y,0.7,0,Math.PI*2); ctx.fill();
+ // Ring tracks
+ for(var ri=1;ri0){ctx.font='6.5px Share Tech Mono,monospace';ctx.fillStyle='rgba('+rd.rgb+',0.28)';ctx.fillText(zOn+'/'+zTot,cx+r+5,cy+11);}
+ ctx.textAlign='left';
}
- // Orbital ring tracks
- NM_RINGS.slice(1).forEach((rd, i) => {
- const ri = i+1, r = minDim * rd.rFrac;
- const hasOn = _nmNodes.some(n=>n.ringIdx===ri&&n.online);
- ctx.beginPath(); ctx.arc(cx,cy,r,0,Math.PI*2);
- ctx.strokeStyle = hasOn ? `rgba(${rd.rgb},0.12)` : 'rgba(60,60,80,0.08)';
- ctx.lineWidth=0.8; ctx.setLineDash([3,9]); ctx.stroke(); ctx.setLineDash([]);
+ // Compute node positions
+ var pos=[];
+ for(var i=0;i<_nmNodes.length;i++) pos.push(_nmNodePos(_nmNodes[i],W,H));
- // Tick marks every 30°
- for (let a=0; an.ringIdx===ri&&n.online).length;
- const zTot = _nmNodes.filter(n=>n.ringIdx===ri).length;
- ctx.font='700 8px Share Tech Mono,monospace'; ctx.textAlign='left';
- ctx.fillStyle=`rgba(${rd.rgb},0.42)`;
- ctx.fillText(rd.label, cx+r+6, cy+2);
- if (zTot>0) {
- ctx.font='6.5px Share Tech Mono,monospace';
- ctx.fillStyle=`rgba(${rd.rgb},0.28)`;
- ctx.fillText(`${zOn}/${zTot}`, cx+r+6, cy+11);
- }
- ctx.textAlign='left';
- });
-
- // Live node positions for this frame
- const pos = _nmNodes.map(n=>nodePos(n));
-
- // Spoke lines
- _nmEdges.forEach(e => {
- const pa=pos[e.from], pb=pos[e.to]; if(!pa||!pb) return;
- const n=_nmNodes[e.from], rgb=_nodeRgb(n);
- ctx.beginPath(); ctx.moveTo(pa.x,pa.y); ctx.lineTo(pb.x,pb.y);
- ctx.strokeStyle = n.online ? `rgba(${rgb},0.1)` : 'rgba(80,20,30,0.07)';
+ // Spokes
+ for(var ei=0;ei<_nmEdges.length;ei++){
+ var e=_nmEdges[ei], pa=pos[e.from], pb=pos[e.to]; if(!pa||!pb) continue;
+ var n=_nmNodes[e.from], rgb=_nmRgb(n);
+ ctx.beginPath();ctx.moveTo(pa.x,pa.y);ctx.lineTo(pb.x,pb.y);
+ ctx.strokeStyle=n.online?'rgba('+rgb+',0.1)':'rgba(80,20,30,0.07)';
ctx.lineWidth=e.strength*0.9; ctx.stroke();
- });
+ }
// Particles
- _nmParticles.forEach(p => {
- p.t=(p.t+p.speed)%1;
- const e=_nmEdges[p.edge]; if(!e) return;
- const pa=pos[e.from], pb=pos[e.to]; if(!pa||!pb) return;
- if(!_nmNodes[e.from]?.online) return;
- const t = p.dir==='in' ? p.t : 1-p.t;
- const px=pa.x+(pb.x-pa.x)*t, py=pa.y+(pb.y-pa.y)*t;
- const fade=Math.min(t*8,(1-t)*8,1);
- ctx.beginPath(); ctx.arc(px,py,p.r,0,Math.PI*2);
- if(p.dir==='in'){
- ctx.fillStyle=`rgba(0,210,255,${(0.6+Math.sin(_nmT*3+p.t*10)*0.3)*fade})`;
- ctx.shadowColor='rgba(0,200,255,0.7)';
- } else {
- ctx.fillStyle=`rgba(255,130,0,${(0.5+Math.sin(_nmT*4+p.t*8)*0.25)*fade})`;
- ctx.shadowColor='rgba(255,110,0,0.6)';
- }
- ctx.shadowBlur=6; ctx.fill(); ctx.shadowBlur=0;
- });
+ for(var pi=0;pi<_nmParticles.length;pi++){
+ var p=_nmParticles[pi]; p.t=(p.t+p.speed)%1;
+ var e=_nmEdges[p.edge]; if(!e) continue;
+ var pa=pos[e.from],pb=pos[e.to]; if(!pa||!pb) continue;
+ if(!_nmNodes[e.from].online) continue;
+ var t=p.dir==='in'?p.t:1-p.t;
+ var px=pa.x+(pb.x-pa.x)*t, py=pa.y+(pb.y-pa.y)*t;
+ var fade=Math.min(t*8,(1-t)*8,1);
+ ctx.beginPath();ctx.arc(px,py,p.r,0,Math.PI*2);
+ if(p.dir==='in'){ctx.fillStyle='rgba(0,210,255,'+(((0.6+Math.sin(_nmT*3+p.t*10)*0.3)*fade).toFixed(3))+')';ctx.shadowColor='rgba(0,200,255,0.7)';}
+ else{ctx.fillStyle='rgba(255,130,0,'+(((0.5+Math.sin(_nmT*4+p.t*8)*0.25)*fade).toFixed(3))+')';ctx.shadowColor='rgba(255,110,0,0.6)';}
+ ctx.shadowBlur=6;ctx.fill();ctx.shadowBlur=0;
+ }
// Nodes
- _nmNodes.forEach((n,ni) => {
- const p=pos[ni], rgb=_nodeRgb(n);
- const pulse=0.5+Math.sin(_nmT*1.5+n.pulse)*0.3;
- const isHub=n.ring==='hub', isHov=_nmHoverNode===ni;
- const baseR=n.r+(isHov?5:0);
-
+ for(var ni=0;ni<_nmNodes.length;ni++){
+ var n=_nmNodes[ni], p=pos[ni], rgb=_nmRgb(n);
+ var pulse=0.5+Math.sin(_nmT*1.5+n.pulse)*0.3;
+ var isHub=n.ringIdx===0, isHov=_nmHoverNode===ni, baseR=n.r+(isHov?5:0);
+ // Glow
if(n.online){
- const gr=baseR+10+Math.sin(_nmT*1.2+n.pulse)*3;
- const grd=ctx.createRadialGradient(p.x,p.y,baseR*0.3,p.x,p.y,gr);
- grd.addColorStop(0,`rgba(${rgb},${pulse*0.22})`); grd.addColorStop(1,`rgba(${rgb},0)`);
- ctx.beginPath(); ctx.arc(p.x,p.y,gr,0,Math.PI*2); ctx.fillStyle=grd; ctx.fill();
+ var gr=baseR+10+Math.sin(_nmT+n.pulse)*3;
+ var g=ctx.createRadialGradient(p.x,p.y,baseR*0.3,p.x,p.y,gr);
+ g.addColorStop(0,'rgba('+rgb+','+(pulse*0.22).toFixed(3)+')');g.addColorStop(1,'rgba('+rgb+',0)');
+ ctx.beginPath();ctx.arc(p.x,p.y,gr,0,Math.PI*2);ctx.fillStyle=g;ctx.fill();
+ // Rotating arc
+ var arcSpd=isHub?0.6:0.38,arcLen=isHub?Math.PI*1.5:Math.PI*0.85;
+ ctx.beginPath();ctx.arc(p.x,p.y,baseR+4,_nmT*arcSpd,_nmT*arcSpd+arcLen);
+ ctx.strokeStyle='rgba('+rgb+','+(0.2+pulse*0.15).toFixed(3)+')';ctx.lineWidth=isHub?1.5:0.8;ctx.stroke();
+ if(isHub){ctx.beginPath();ctx.arc(p.x,p.y,baseR+8,-_nmT*0.35,-_nmT*0.35+Math.PI*1.1);ctx.strokeStyle='rgba('+rgb+',0.12)';ctx.lineWidth=0.6;ctx.stroke();}
}
-
- // Rotating arc decoration
- if(n.online){
- const arcSpd=isHub?0.6:0.38, arcLen=isHub?Math.PI*1.5:Math.PI*0.85;
- ctx.beginPath(); ctx.arc(p.x,p.y,baseR+4,_nmT*arcSpd,_nmT*arcSpd+arcLen);
- ctx.strokeStyle=`rgba(${rgb},${0.2+pulse*0.15})`; ctx.lineWidth=isHub?1.5:0.8; ctx.stroke();
- if(isHub){
- ctx.beginPath(); ctx.arc(p.x,p.y,baseR+8,-_nmT*0.35,-_nmT*0.35+Math.PI*1.1);
- ctx.strokeStyle=`rgba(${rgb},0.12)`; ctx.lineWidth=0.6; ctx.stroke();
- }
- }
-
// Fill + border
- const fg=ctx.createRadialGradient(p.x,p.y,0,p.x,p.y,baseR);
- const fa=n.online?pulse*0.4:0.09;
- fg.addColorStop(0,`rgba(${rgb},${fa})`); fg.addColorStop(1,`rgba(${rgb},${fa*0.2})`);
- ctx.beginPath(); ctx.arc(p.x,p.y,baseR,0,Math.PI*2);
- ctx.fillStyle=fg; ctx.fill();
- ctx.strokeStyle=`rgba(${rgb},${n.online?0.58+pulse*0.3:0.18})`; ctx.lineWidth=isHub?2:1; ctx.stroke();
-
+ var fg=ctx.createRadialGradient(p.x,p.y,0,p.x,p.y,baseR);
+ var fa=n.online?pulse*0.4:0.09;
+ fg.addColorStop(0,'rgba('+rgb+','+fa.toFixed(3)+')');fg.addColorStop(1,'rgba('+rgb+','+(fa*0.2).toFixed(3)+')');
+ ctx.beginPath();ctx.arc(p.x,p.y,baseR,0,Math.PI*2);ctx.fillStyle=fg;ctx.fill();
+ ctx.strokeStyle='rgba('+rgb+','+(n.online?0.58+pulse*0.3:0.18).toFixed(3)+')';ctx.lineWidth=isHub?2:1;ctx.stroke();
// Hub crosshairs
if(isHub){
- ctx.strokeStyle=`rgba(${rgb},0.2)`; ctx.lineWidth=0.6;
- [[p.x-44,p.y,p.x-baseR-3,p.y],[p.x+baseR+3,p.y,p.x+44,p.y],
- [p.x,p.y-44,p.x,p.y-baseR-3],[p.x,p.y+baseR+3,p.x,p.y+44]
- ].forEach(([x1,y1,x2,y2])=>{ ctx.beginPath();ctx.moveTo(x1,y1);ctx.lineTo(x2,y2);ctx.stroke(); });
+ ctx.strokeStyle='rgba('+rgb+',0.2)';ctx.lineWidth=0.6;
+ var lines=[[p.x-42,p.y,p.x-baseR-3,p.y],[p.x+baseR+3,p.y,p.x+42,p.y],[p.x,p.y-42,p.x,p.y-baseR-3],[p.x,p.y+baseR+3,p.x,p.y+42]];
+ for(var li=0;li0.2?'left': ca<-0.2?'right':'center';
- ctx.fillStyle=n.online?`rgba(${rgb},0.92)`:'rgba(200,80,80,0.65)';
+ // Status dot
+ if(!isHub){ctx.beginPath();ctx.arc(p.x+baseR*0.65,p.y-baseR*0.65,2.2,0,Math.PI*2);ctx.fillStyle=n.online?'rgba(0,255,120,0.9)':'rgba(255,50,80,0.9)';ctx.fill();}
+ // Label outward
+ var la=isHub?-Math.PI/2:Math.atan2(p.y-cy,p.x-cx);
+ var ld=baseR+(isHub?14:10);
+ var lx=p.x+Math.cos(la)*ld, ly=p.y+Math.sin(la)*ld+3;
+ ctx.font=(isHub?'700 10':'7.5')+'px Share Tech Mono,monospace';
+ var ca=Math.cos(la);
+ ctx.textAlign=isHub?'center':ca>0.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(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);}
ctx.textAlign='left';
- });
+ }
}
- draw();
+ frame();
canvas.onmousemove=function(e){
- const r=canvas.getBoundingClientRect(), mx=e.clientX-r.left, my=e.clientY-r.top;
- const positions=_nmNodes.map(n=>nodePos(n));
- let found=-1;
- positions.forEach((p,i)=>{ if(Math.hypot(p.x-mx,p.y-my)<_nmNodes[i].r+10) found=i; });
+ var rect=canvas.getBoundingClientRect(), mx=e.clientX-rect.left, my=e.clientY-rect.top;
+ var found=-1;
+ for(var i=0;i<_nmNodes.length;i++){var p=_nmNodePos(_nmNodes[i],W,H);if(Math.sqrt((p.x-mx)*(p.x-mx)+(p.y-my)*(p.y-my))<_nmNodes[i].r+10){found=i;break;}}
_nmHoverNode=found>=0?found:null;
- const info=document.getElementById('nmNodeInfo'); if(!info) return;
+ var info=document.getElementById('nmNodeInfo'); if(!info) return;
if(found>=0){
- const n=_nmNodes[found], rgb=_nodeRgb(n);
- document.getElementById('ni-name').textContent=n.label;
- document.getElementById('ni-name').style.color=`rgb(${rgb})`;
+ var n=_nmNodes[found],rgb=_nmRgb(n);
+ document.getElementById('ni-name').textContent=n.label; document.getElementById('ni-name').style.color='rgb('+rgb+')';
document.getElementById('ni-ip').textContent='IP: '+(n.sub||'—');
document.getElementById('ni-status').textContent='STATUS: '+(n.online?'ONLINE':'OFFLINE');
- document.getElementById('ni-type').textContent='TYPE: '+n.ring.toUpperCase();
- info.style.display='block'; info.style.left=(mx+16)+'px'; info.style.top=(my-6)+'px';
+ document.getElementById('ni-type').textContent='RING: '+(NM_RINGS[n.ringIdx]?NM_RINGS[n.ringIdx].name.toUpperCase():'HUB');
+ info.style.display='block'; info.style.left=(mx+14)+'px'; info.style.top=(my-6)+'px';
} else { info.style.display='none'; }
};
- canvas.onmouseleave=()=>{ _nmHoverNode=null; const i=document.getElementById('nmNodeInfo'); if(i) i.style.display='none'; };
+ canvas.onmouseleave=function(){_nmHoverNode=null;var i=document.getElementById('nmNodeInfo');if(i)i.style.display='none';};
}
// ── GLOBALS ──────────────────────────────────────────────────────────
@@ -2974,33 +2867,23 @@ async function sendMessage() {
const text = input.value.trim();
if (!text) return;
- // Local commands — handled client-side without API round-trip
- const t = text.toLowerCase();
-
- // Network map open
- if (NM_OPEN_RE.test(t)) {
- input.value = '';
- addMessage('user', text);
- addMessage('jarvis', 'Launching network topology display. Stand by.');
+ // Local commands — no API round-trip
+ var t2 = text.toLowerCase();
+ if (NM_OPEN_RE.test(t2)) {
+ input.value=''; addMessage('user',text);
+ addMessage('jarvis','Launching network topology display.');
speak('Launching network topology display.');
- openNetMap();
+ openNetMap(); return;
+ }
+ if (NM_CLOSE_RE.test(t2)) {
+ input.value=''; addMessage('user',text);
+ var isOpen=document.getElementById('netMapOverlay')&&document.getElementById('netMapOverlay').classList.contains('nm-open');
+ if(isOpen){closeNetMap();addMessage('jarvis','Network map closed.');speak('Network map closed.');}
+ else addMessage('jarvis','Network map is not currently active.');
return;
}
- // Network map close
- if (NM_CLOSE_RE.test(t)) {
- input.value = '';
- addMessage('user', text);
- const isOpen = document.getElementById('netMapOverlay')?.classList.contains('nm-open');
- if (isOpen) {
- closeNetMap();
- addMessage('jarvis', 'Network topology display closed.');
- speak('Network topology display closed.');
- } else {
- addMessage('jarvis', 'Network map is not currently active.');
- }
- return;
- }
-
+ // Local panel-toggle voice commands (handled without API call)
+ const t = text.toLowerCase();
if (/\b(focus\s*mode|hide\s*(panels?|stats?|statistics)|full\s*screen\s*jarvis)\b/.test(t)) {
input.value = '';
addMessage('user', text);