// ── 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:[]}; // Deduplicate agent devices by hostname (same logical host registered twice) var _seenNames={}; var _dedupedDevices=[]; for(var i=0;i(prev.last_seen||'')){_dedupedDevices[_seenNames[nk].idx]=d;_seenNames[nk].d=d;} } } // Bucket: offline agents are excluded from inner rings (keep DB/netscan as-is) for(var i=0;i<_dedupedDevices.length;i++){ var d=_dedupedDevices[i]; var isOffline=!(d.alive||d.status==='online'); if(d.source==='agent'&&isOffline) continue; // hide offline agents from map buckets[_nmClassify(d)].push(d); } // 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;ri0.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;i0){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=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';}; }