Add sleep mode + window focus on wake

Sleep mode:
- Commands: good night / go to sleep / shut down / standby / offline / signing off
- Works via voice or text
- UI dims to 8% brightness, slow-spinning standby reactor overlay appears
- Refresh loop pauses (light 2min heartbeat keeps session alive)
- Mic stays fully active — only responds to master wake phrases
- Idle-reload disabled while sleeping (prevents unwanted reloads overnight)

Wake from sleep (master wake phrase):
- Detects wake phrase while isAsleep=true, routes to wakeFromSleep()
- Full HUD boot sequence animation (panels slide in)
- refreshAll() fires immediately to reload all data
- JARVIS greets: All systems back online

Window focus on any wake:
- window.focus() called on every enterVoiceMode
- document.title flashes 8x between JARVIS ONLINE and default
- Web Notifications API: system popup fires when window is minimized/backgrounded
- Notification permission requested 3s after login
- Works regardless of sleep/voice mode
This commit is contained in:
2026-06-02 02:45:41 +00:00
parent 381977ed1e
commit 13792b3ced
+167 -12
View File
@@ -844,6 +844,37 @@ body::after{
font-size:0.6rem;display:none;min-width:150px;box-shadow:0 0 18px rgba(0,212,255,0.12)}
#nmNodeInfo .ni-title{color:var(--cyan);font-size:0.62rem;letter-spacing:2px;margin-bottom:3px}
#nmNodeInfo .ni-row{color:var(--text-dim);margin:2px 0}
/* ── SLEEP MODE ──────────────────────────────────────────────────────── */
#sleepOverlay{
position:fixed;inset:0;z-index:500;display:none;
flex-direction:column;align-items:center;justify-content:center;
background:rgba(0,2,10,0.94);
backdrop-filter:blur(6px);
}
#sleepOverlay.active{display:flex}
@keyframes sleepPulse{0%,100%{opacity:0.25;transform:scale(1)}50%{opacity:0.6;transform:scale(1.06)}}
@keyframes sleepCoreGlow{0%,100%{box-shadow:0 0 30px rgba(0,212,255,0.15),0 0 60px rgba(0,212,255,0.05)}50%{box-shadow:0 0 50px rgba(0,212,255,0.3),0 0 100px rgba(0,212,255,0.1)}}
.sleep-reactor{position:relative;width:120px;height:120px;margin-bottom:40px}
.sleep-ring{position:absolute;border-radius:50%;border:1px solid rgba(0,212,255,0.2);top:50%;left:50%;transform:translate(-50%,-50%)}
.sleep-ring.sr1{width:120px;height:120px;animation:spinRing 18s linear infinite;border-color:rgba(0,212,255,0.12)}
.sleep-ring.sr2{width:85px;height:85px;animation:spinRing 12s linear infinite reverse;border-color:rgba(0,212,255,0.18)}
.sleep-ring.sr3{width:52px;height:52px;animation:spinRing 8s linear infinite;border-color:rgba(0,80,160,0.3)}
.sleep-core{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
width:24px;height:24px;border-radius:50%;
background:radial-gradient(circle,rgba(0,150,220,0.6) 0%,rgba(0,80,160,0.3) 60%,transparent 100%);
animation:sleepCoreGlow 4s ease-in-out infinite,sleepPulse 4s ease-in-out infinite}
.sleep-label{font-family:var(--font-display);font-size:0.6rem;letter-spacing:6px;
color:rgba(0,212,255,0.35);animation:sleepPulse 4s ease-in-out infinite;margin-bottom:12px}
.sleep-sub{font-family:var(--font-mono);font-size:0.55rem;letter-spacing:3px;
color:rgba(0,212,255,0.2);animation:sleepPulse 4s ease-in-out infinite;animation-delay:0.5s}
/* App dims on sleep */
#app.sleeping #mainLayout,#app.sleeping #topBar,#app.sleeping #bottomBar{
pointer-events:none;
filter:brightness(0.08) saturate(0.3);
transition:filter 1.2s ease;
}
#app.sleeping #sleepOverlay{display:flex}
</style>
</head>
<body>
@@ -1118,6 +1149,18 @@ body::after{
</div>
<div id="nmNodeInfo"><div class="ni-title" id="ni-name"></div><div class="ni-row" id="ni-ip"></div><div class="ni-row" id="ni-status"></div><div class="ni-row" id="ni-type"></div></div>
</div>
<!-- SLEEP OVERLAY -->
<div id="sleepOverlay">
<div class="sleep-reactor">
<div class="sleep-ring sr1"></div>
<div class="sleep-ring sr2"></div>
<div class="sleep-ring sr3"></div>
<div class="sleep-core"></div>
</div>
<div class="sleep-label">JARVIS — STANDBY</div>
<div class="sleep-sub">SAY "WAKE UP JARVIS" TO RESUME</div>
</div>
<div id="sitesModal" style="position:fixed;inset:0;background:rgba(0,0,0,0.92);z-index:9999;display:none;align-items:flex-start;justify-content:center;padding:24px;overflow-y:auto">
<div style="background:var(--panel-bg);border:1px solid var(--panel-border);width:100%;max-width:960px;font-family:var(--font-mono)">
<div style="display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-bottom:1px solid var(--panel-border)">
@@ -1713,6 +1756,93 @@ function _drawTopo() {
})();
// ── SLEEP MODE ────────────────────────────────────────────────────────────────
var isAsleep = false;
var _sleepRefreshTimer = null;
var SLEEP_CMDS = /\b(good\s*night|go\s*to\s*sleep|sleep\s*(mode|now)?|shut\s*(down|off)\s*(jarvis|for\s*the\s*night)?|offline|stand\s*by|power\s*down|hibernate|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);
// Browser Notification API — fires even when window is minimized
if ('Notification' in window) {
if (Notification.permission === 'granted') {
new Notification('JARVIS', { body: 'Wake word detected — system online.', icon: '/favicon.ico', tag: 'jarvis-wake', requireInteraction: false });
} else if (Notification.permission !== 'denied') {
Notification.requestPermission();
}
}
}
// ── NETWORK MAP ──────────────────────────────────────────────────────────────
var _nmNodes=[], _nmEdges=[], _nmParticles=[], _nmRaf=null, _nmT=0, _nmHoverNode=null;
var _nmRot=[0,0,0,0,0];
@@ -2059,7 +2189,7 @@ function showApp(name, greeting, silent = false) {
refreshAll();
refreshTimer = setInterval(refreshAll, 10000); // every 10s
setInterval(() => {
if (Date.now() - lastActivity > IDLE_RELOAD_MS) {
if (!isAsleep && Date.now() - lastActivity > IDLE_RELOAD_MS) {
sessionStorage.setItem('jarvis_autoreload', '1');
location.reload();
}
@@ -2085,6 +2215,10 @@ function showApp(name, greeting, silent = false) {
}
}, 12000);
startListening();
// Request notification permission for wake-word alerts when minimized
if ('Notification' in window && Notification.permission === 'default') {
setTimeout(() => Notification.requestPermission(), 3000);
}
loadNetwork();
loadHA();
checkAgentStatus();
@@ -2869,6 +3003,15 @@ async function sendMessage() {
// Local commands — no API round-trip
var t2 = text.toLowerCase();
// Sleep command
if (SLEEP_CMDS.test(t2)) {
input.value = '';
addMessage('user', text);
enterSleepMode();
return;
}
if (NM_OPEN_RE.test(t2)) {
input.value=''; addMessage('user',text);
addMessage('jarvis','Launching network topology display.');
@@ -2948,16 +3091,28 @@ function initVoice() {
const transcript = (e.results[0][0].transcript || '').trim();
if (!transcript) return;
const lc = transcript.toLowerCase();
// Sleeping: ONLY respond to master wake phrases
if (isAsleep) {
if (WAKE_PHRASES.some(p => lc.includes(p))) wakeFromSleep();
return;
}
if (!voiceMode) {
if (WAKE_PHRASES.some(p => lc.includes(p))) enterVoiceMode();
} else if (!voiceMuted) {
// Awake — any speech is a command; strip optional "jarvis" prefix
voiceLastCmd = Date.now();
voiceActive = Date.now();
const cmd = lc.startsWith(CMD_PREFIX)
? transcript.substring(CMD_PREFIX.length).trim()
: transcript;
if (cmd) {
// Check for sleep command by voice
if (SLEEP_CMDS.test(cmd)) {
addMessage('user', transcript);
enterSleepMode();
return;
}
_showTranscript(cmd);
document.getElementById('textInput').value = cmd;
sendMessage();
@@ -2991,21 +3146,21 @@ function _showTranscript(text) {
if (el) { el.placeholder = '▶ ' + text.substring(0, 60); setTimeout(() => { el.placeholder = 'Enter command or speak to JARVIS...'; }, 3000); }
}
function enterVoiceMode() {
function enterVoiceMode(source) {
voiceMode = true;
voiceMuted = false;
voiceLastCmd = Date.now();
voiceActive = Date.now();
updateMicBtn();
speak('Yes, ' + (sessionUser || 'Sir') + '?');
// Bring window to front and maximize when JARVIS wakes
try {
window.focus();
if (!document.fullscreenElement && window.screen) {
window.moveTo(0, 0);
window.resizeTo(window.screen.availWidth, window.screen.availHeight);
}
} catch(e) {}
// Focus/notify when woken from minimized or sleep
_focusWindow();
if (source === 'wake') {
const g = 'All systems back online, ' + (sessionUser || 'Sir') + '. Good to have you back.';
addMessage('jarvis', g);
speak(g);
} else {
speak('Yes, ' + (sessionUser || 'Sir') + '?');
}
}
function exitVoiceMode() {