mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
462ce257a8
Split monolithic 261KB index.html into maintainable modules: - assets/css/jarvis.css (65KB, 1103 lines) — all styles - assets/js/jarvis-effects.js (23KB) — particle canvas, sparklines, panel float, face tracking, glitch - assets/js/jarvis-overlays.js (17KB) — sleep mode, network map - assets/js/jarvis-app.js (60KB) — globals, init, login, API, panels, chat, voice, alerts, weather, news, planner - assets/js/jarvis-protocols.js (69KB) — arc reactor, intel/comms/guardian/mission/directives/clearance/sites/vision, history search, suggestions, mobile index.html is now a 25KB thin HTML shell with link/script tags. Load order preserved; all cross-file dependencies resolve at runtime after window.load. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
358 lines
17 KiB
JavaScript
358 lines
17 KiB
JavaScript
// ── SLEEP MODE ────────────────────────────────────────────────────────────────
|
|
var isAsleep = false;
|
|
var _sleepRefreshTimer = null;
|
|
|
|
var SLEEP_CMDS = /\b(good\s*night(\s*jarvis)?|go\s*to\s*sleep|sleep\s*mode|shut\s*(down|off)\s*(jarvis|for\s*the\s*night)|go\s*offline|going\s*offline|jarvis\s*(go\s*)?(offline|sleep|shutdown)|stand\s*by\s*mode|power\s*down(\s*jarvis)?|signing\s*off)\b/i;
|
|
|
|
function enterSleepMode() {
|
|
if (isAsleep) return;
|
|
isAsleep = true;
|
|
|
|
// Pause voice mode
|
|
voiceMode = false;
|
|
voiceMuted = false;
|
|
updateMicBtn();
|
|
|
|
// Slow or pause the refresh loop — keep mic alive for wake word
|
|
clearInterval(refreshTimer);
|
|
refreshTimer = null;
|
|
// Light polling every 2 min just to stay alive
|
|
_sleepRefreshTimer = setInterval(function() {
|
|
// heartbeat only — keep session alive without hammering APIs
|
|
try { fetch('/api/auth', {method:'GET', headers:{'Authorization':'Bearer '+sessionToken}}); } catch(e) {}
|
|
}, 120000);
|
|
|
|
// Dim the UI
|
|
var app = document.getElementById('app');
|
|
if (app) app.classList.add('sleeping');
|
|
|
|
// Flash title to confirm
|
|
document.title = 'JARVIS — STANDBY';
|
|
|
|
addMessage('jarvis', 'Understood. Going offline. Say "wake up JARVIS" when you need me.');
|
|
}
|
|
|
|
function wakeFromSleep() {
|
|
if (!isAsleep) return;
|
|
isAsleep = false;
|
|
|
|
// Restore full polling
|
|
clearInterval(_sleepRefreshTimer);
|
|
_sleepRefreshTimer = null;
|
|
refreshAll();
|
|
refreshTimer = setInterval(refreshAll, 10000);
|
|
|
|
// Remove dim overlay
|
|
var app = document.getElementById('app');
|
|
if (app) app.classList.remove('sleeping');
|
|
document.title = 'JARVIS — Integrated Defense and Logistics System';
|
|
|
|
// Boot sequence
|
|
var topBar=document.getElementById('topBar'), lp=document.getElementById('leftPanel');
|
|
var rp=document.getElementById('rightPanel'), cp=document.getElementById('centerPanel');
|
|
[topBar,lp,rp,cp].forEach(function(el){if(el){el.style.opacity='0';}});
|
|
requestAnimationFrame(function(){
|
|
setTimeout(function(){if(topBar){topBar.style.opacity='';topBar.classList.add('boot-top');}},0);
|
|
setTimeout(function(){if(lp){lp.style.opacity='';lp.classList.add('boot-left');}},140);
|
|
setTimeout(function(){if(rp){rp.style.opacity='';rp.classList.add('boot-right');}},200);
|
|
setTimeout(function(){if(cp){cp.style.opacity='';cp.classList.add('boot-center');}},260);
|
|
setTimeout(function(){[topBar,lp,rp,cp].forEach(function(el){if(el)el.classList.remove('boot-top','boot-left','boot-right','boot-center');});},1400);
|
|
});
|
|
|
|
// Enter voice mode and greet
|
|
enterVoiceMode('wake');
|
|
}
|
|
|
|
function _focusWindow() {
|
|
// Attempt to bring browser window to front
|
|
try { window.focus(); } catch(e) {}
|
|
|
|
// Flash title to grab attention if tab is backgrounded
|
|
var _origTitle = 'JARVIS — Integrated Defense and Logistics System';
|
|
var _flashCount = 0;
|
|
var _titleFlash = setInterval(function() {
|
|
document.title = _flashCount % 2 === 0 ? '⚡ JARVIS — ONLINE' : _origTitle;
|
|
if (++_flashCount >= 8) { clearInterval(_titleFlash); document.title = _origTitle; }
|
|
}, 400);
|
|
|
|
// Notify if already have permission — never request during voice activity
|
|
if ('Notification' in window && Notification.permission === 'granted') {
|
|
try { new Notification('JARVIS', { body: 'Wake word detected.', tag: 'jarvis-wake', requireInteraction: false }); } catch(e) {}
|
|
}
|
|
}
|
|
|
|
// ── NETWORK MAP ──────────────────────────────────────────────────────────────
|
|
var _nmNodes=[], _nmEdges=[], _nmParticles=[], _nmRaf=null, _nmT=0, _nmHoverNode=null;
|
|
var _nmRot=[0,0,0,0,0,0];
|
|
var NM_RINGS=[
|
|
{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|panel|status|scan|view|overlay))?|topology|node\s*map)\b|\bnetwork\s*(map|topology|viz|visual|graph)\b|\b(show|open|display|launch|pull\s*up|bring\s*up)\s+(?:me\s+)?(?:the\s+)?network\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'){
|
|
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,160,200';
|
|
if(n.ringIdx===5) return '0,110,170';
|
|
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};
|
|
}
|
|
|
|
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:NM_RINGS[0].nodeR,pulse:0});
|
|
// Bucket
|
|
var buckets={proxmox:[],services:[],agents:[],devices:[],network:[]};
|
|
for(var i=0;i<devices.length;i++) buckets[_nmClassify(devices[i])].push(devices[i]);
|
|
// Sort netscan devices: online first, then those with meaningful hostnames
|
|
buckets.network.sort(function(a,b){
|
|
var sa=a.alive?1:0, sb=b.alive?1:0;
|
|
if(sb!==sa) return sb-sa;
|
|
var ha=(a.name&&a.name!==a.ip&&a.name.indexOf('10.48')!==0)?1:0;
|
|
var hb=(b.name&&b.name!==b.ip&&b.name.indexOf('10.48')!==0)?1:0;
|
|
return hb-ha;
|
|
});
|
|
var rings=['proxmox','services','agents','devices','network'];
|
|
for(var ri=0;ri<rings.length;ri++){
|
|
var rname=rings[ri], rd=NM_RINGS[ri+1], list=buckets[rname].slice(0,rd.cap);
|
|
for(var j=0;j<list.length;j++){
|
|
var d=list[j];
|
|
var baseA=(ri%2===0?-Math.PI/2:-Math.PI/3);
|
|
var angle=baseA+(j/Math.max(list.length,1))*Math.PI*2;
|
|
_nmNodes.push({
|
|
id:d.agent_id||d.ip||(rname+j),
|
|
label:(d.name||d.ip||'?').replace(/_[a-f0-9]{6,}$/,'').substring(0,11),
|
|
sub:d.ip||'', online:!!(d.alive||d.status==='online'),
|
|
agent:d.source==='agent', ringIdx:ri+1, angle:angle,
|
|
r:rd.nodeR, pulse:Math.random()*Math.PI*2,
|
|
});
|
|
}
|
|
}
|
|
// Edges + particles
|
|
for(var i=1;i<_nmNodes.length;i++){
|
|
var n=_nmNodes[i], str=n.agent?0.9:0.45;
|
|
_nmEdges.push({from:i,to:0,strength:str});
|
|
if(n.online){
|
|
var cnt=n.agent?3:2;
|
|
for(var p=0;p<cnt;p++) _nmParticles.push({edge:_nmEdges.length-1,t:Math.random(),dir:'in',speed:0.002+Math.random()*0.0025,r:1.8+Math.random()*0.9});
|
|
if(n.agent&&Math.random()>0.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});
|
|
}
|
|
}
|
|
// Stats
|
|
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-48 : 490;
|
|
var W=canvas.width, H=canvas.height, cx=W/2, cy=H/2, minR=Math.min(cx,cy);
|
|
var ctx=canvas.getContext('2d');
|
|
|
|
function frame(){
|
|
if(!document.getElementById('netMapOverlay').classList.contains('nm-open')) return;
|
|
_nmRaf=requestAnimationFrame(frame);
|
|
_nmT+=0.016;
|
|
for(var i=0;i<NM_RINGS.length;i++) _nmRot[i]=(_nmRot[i]||0)+NM_RINGS[i].speed;
|
|
ctx.clearRect(0,0,W,H);
|
|
|
|
// Dot grid
|
|
ctx.fillStyle='rgba(0,180,255,0.05)';
|
|
for(var gx=26;gx<W;gx+=38) for(var gy=26;gy<H;gy+=38){ctx.beginPath();ctx.arc(gx,gy,0.7,0,Math.PI*2);ctx.fill();}
|
|
|
|
// Ring tracks
|
|
for(var ri=1;ri<NM_RINGS.length;ri++){
|
|
var rd=NM_RINGS[ri], r=minR*rd.rFrac;
|
|
var hasOn=false;
|
|
for(var i=0;i<_nmNodes.length;i++) if(_nmNodes[i].ringIdx===ri&&_nmNodes[i].online){hasOn=true;break;}
|
|
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([]);
|
|
// Ticks
|
|
for(var a=0;a<Math.PI*2;a+=Math.PI/6){
|
|
ctx.beginPath();ctx.moveTo(cx+Math.cos(a)*(r-3),cy+Math.sin(a)*(r-3));ctx.lineTo(cx+Math.cos(a)*(r+3),cy+Math.sin(a)*(r+3));
|
|
ctx.strokeStyle='rgba('+rd.rgb+',0.18)';ctx.lineWidth=0.6;ctx.stroke();
|
|
}
|
|
// Label at 3-o'clock
|
|
var zOn=0,zTot=0;
|
|
for(var i=0;i<_nmNodes.length;i++){if(_nmNodes[i].ringIdx===ri){zTot++;if(_nmNodes[i].online)zOn++;}}
|
|
ctx.font='700 8px Share Tech Mono,monospace'; ctx.textAlign='left';
|
|
ctx.fillStyle='rgba('+rd.rgb+',0.4)'; ctx.fillText(rd.label,cx+r+5,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+5,cy+11);}
|
|
ctx.textAlign='left';
|
|
}
|
|
|
|
// Compute node positions
|
|
var pos=[];
|
|
for(var i=0;i<_nmNodes.length;i++) pos.push(_nmNodePos(_nmNodes[i],W,H));
|
|
|
|
// 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);
|
|
// Apply float offset to spoke endpoints
|
|
var fya=Math.sin(_nmT*0.85+_nmNodes[e.from].pulse)*4;
|
|
var fyb=Math.sin(_nmT*0.85+_nmNodes[e.to].pulse)*4;
|
|
var lg=ctx.createLinearGradient(pa.x,pa.y+fya,pb.x,pb.y+fyb);
|
|
if(n.online){lg.addColorStop(0,'rgba('+rgb+',0.22)');lg.addColorStop(0.5,'rgba('+rgb+',0.08)');lg.addColorStop(1,'rgba(0,212,255,0.15)');}
|
|
else{lg.addColorStop(0,'rgba(80,20,30,0.07)');lg.addColorStop(1,'rgba(80,20,30,0.07)');}
|
|
ctx.beginPath();ctx.moveTo(pa.x,pa.y+fya);ctx.lineTo(pb.x,pb.y+fyb);
|
|
ctx.strokeStyle=lg;ctx.lineWidth=e.strength*1.1;ctx.stroke();
|
|
}
|
|
|
|
// Particles
|
|
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;
|
|
}
|
|
|
|
// Bubble nodes
|
|
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.4+n.pulse)*0.3;
|
|
var isHub=n.ringIdx===0, isHov=_nmHoverNode===ni;
|
|
// Float offset — each node drifts on Y with unique phase
|
|
var fy=Math.sin(_nmT*0.85+n.pulse)*4;
|
|
var px=p.x, py=p.y+fy;
|
|
var baseR=n.r*(isHov?1.28:1.0);
|
|
|
|
// Ambient glow bloom
|
|
if(n.online){
|
|
var bloomR=baseR*2.6+Math.sin(_nmT*1.1+n.pulse)*3;
|
|
var bloom=ctx.createRadialGradient(px,py,baseR*0.4,px,py,bloomR);
|
|
bloom.addColorStop(0,'rgba('+rgb+','+(pulse*0.2).toFixed(3)+')');
|
|
bloom.addColorStop(1,'rgba('+rgb+',0)');
|
|
ctx.beginPath();ctx.arc(px,py,bloomR,0,Math.PI*2);ctx.fillStyle=bloom;ctx.fill();
|
|
|
|
// Sonar ping — expanding ring that fades out
|
|
var pingR=baseR+(((_nmT*0.6+n.pulse)%1)*baseR*2.5);
|
|
var pingA=(1-(pingR-baseR)/(baseR*2.5))*0.3;
|
|
ctx.beginPath();ctx.arc(px,py,pingR,0,Math.PI*2);
|
|
ctx.strokeStyle='rgba('+rgb+','+pingA.toFixed(3)+')';
|
|
ctx.lineWidth=0.7;ctx.stroke();
|
|
}
|
|
|
|
// Frosted glass fill
|
|
var fg=ctx.createRadialGradient(px,py-baseR*0.28,0,px,py,baseR);
|
|
var fa=n.online?0.17+pulse*0.1:0.06;
|
|
fg.addColorStop(0,'rgba('+rgb+','+(fa*2.0).toFixed(3)+')');
|
|
fg.addColorStop(0.55,'rgba('+rgb+','+fa.toFixed(3)+')');
|
|
fg.addColorStop(1,'rgba('+rgb+','+(fa*0.15).toFixed(3)+')');
|
|
ctx.beginPath();ctx.arc(px,py,baseR,0,Math.PI*2);ctx.fillStyle=fg;ctx.fill();
|
|
|
|
// Glassy highlight sheen (top-left arc)
|
|
if(n.online){
|
|
var sh=ctx.createRadialGradient(px-baseR*0.3,py-baseR*0.35,0,px,py,baseR);
|
|
sh.addColorStop(0,'rgba(255,255,255,0.12)');sh.addColorStop(0.45,'rgba(255,255,255,0.03)');sh.addColorStop(1,'rgba(255,255,255,0)');
|
|
ctx.beginPath();ctx.arc(px,py,baseR,0,Math.PI*2);ctx.fillStyle=sh;ctx.fill();
|
|
}
|
|
|
|
// Border
|
|
ctx.beginPath();ctx.arc(px,py,baseR,0,Math.PI*2);
|
|
ctx.strokeStyle='rgba('+rgb+','+(n.online?(0.45+pulse*0.35).toFixed(3):'0.18')+')';
|
|
ctx.lineWidth=isHub?1.8:1.1;ctx.stroke();
|
|
|
|
// Hub crosshairs (softer)
|
|
if(isHub){
|
|
ctx.strokeStyle='rgba('+rgb+',0.15)';ctx.lineWidth=0.6;
|
|
var ext=50;
|
|
var hlines=[[px-ext,py,px-baseR-3,py],[px+baseR+3,py,px+ext,py],[px,py-ext,px,py-baseR-3],[px,py+baseR+3,px,py+ext]];
|
|
for(var li=0;li<hlines.length;li++){ctx.beginPath();ctx.moveTo(hlines[li][0],hlines[li][1]);ctx.lineTo(hlines[li][2],hlines[li][3]);ctx.stroke();}
|
|
}
|
|
|
|
// Status dot
|
|
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';
|
|
}
|
|
}
|
|
frame();
|
|
|
|
canvas.onmousemove=function(e){
|
|
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;
|
|
var info=document.getElementById('nmNodeInfo'); if(!info) return;
|
|
if(found>=0){
|
|
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='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=function(){_nmHoverNode=null;var i=document.getElementById('nmNodeInfo');if(i)i.style.display='none';};
|
|
}
|