Add morning briefing, command palette, and boot animations

#11 Smart Morning Briefing: auto-speaks once per day before noon — fetches
tasks, appointments, active alerts, and weather, composes a ~2-sentence TTS
summary. Stored in localStorage (jarvis_brief_YYYY-MM-DD) to fire only once.

#12 Quick Command Palette (Ctrl+K): frosted-glass overlay with 20 pre-loaded
commands across 6 groups (Network/Agents/Planner/Media/Smart Home/System).
Fuzzy filter as you type, arrow-key navigation, Enter to fire. Matches are
highlighted. Backdrop click or Escape to close.

#13 Live Boot Animation: stat bars and numbers now count from 0 on first render
via tickTo() change. Arc Reactor rings spin in with staggered delays (0.08s
per ring) on login using boot-spin CSS class + @keyframes arcBootSpin.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 03:00:16 +00:00
parent 462ce257a8
commit b2aa3280e1
4 changed files with 252 additions and 1 deletions
+132
View File
@@ -1411,3 +1411,135 @@ function initMobile() {
document.getElementById('mob-btn-left')?.classList.add('active');
}
window.addEventListener('resize', initMobile);
// ── COMMAND PALETTE (Ctrl+K) ──────────────────────────────────────────────
const _PALETTE_COMMANDS = [
{ label: 'Run a network scan', q: 'run a network scan', group: 'Network' },
{ label: 'Show online devices', q: 'who is online on the network', group: 'Network' },
{ label: 'Proxmox status', q: 'proxmox status', group: 'Network' },
{ label: 'Check agent status', q: 'check all agents', group: 'Agents' },
{ label: 'Restart JARVIS agent', q: 'restart jarvis agent', group: 'Agents' },
{ label: 'Check VM resources', q: 'VM resource suggestions', group: 'Agents' },
{ label: 'Daily briefing', q: 'daily briefing', group: 'Planner' },
{ label: 'My tasks today', q: 'my tasks today', group: 'Planner' },
{ label: 'My calendar', q: 'my calendar', group: 'Planner' },
{ label: 'What's playing on Jellyfin', q: 'what is playing on Jellyfin', group: 'Media' },
{ label: 'Pause Jellyfin', q: 'pause Jellyfin', group: 'Media' },
{ label: 'Next track on Jellyfin', q: 'next track on Jellyfin', group: 'Media' },
{ label: 'Stop Jellyfin', q: 'stop Jellyfin', group: 'Media' },
{ label: 'List HA scenes', q: 'show home assistant scenes', group: 'Smart Home'},
{ label: 'Activate scene', q: 'activate scene ', group: 'Smart Home'},
{ label: 'Focus mode', q: 'focus mode', group: 'UI' },
{ label: 'Show all panels', q: 'show all panels', group: 'UI' },
{ label: 'Check alerts', q: 'check alerts', group: 'System' },
{ label: 'Site health', q: 'site health', group: 'System' },
{ label: 'System status', q: 'system status', group: 'System' },
{ label: 'Check inbox', q: 'check inbox', group: 'Comms' },
{ label: 'Search history', q: '', group: 'Chat', search: true },
];
let _paletteOpen = false;
function openPalette() {
if (_paletteOpen) return;
_paletteOpen = true;
const ov = document.getElementById('cmdPalette');
if (!ov) return;
ov.style.display = 'flex';
const inp = document.getElementById('cmdPaletteInput');
inp.value = '';
renderPaletteItems('');
requestAnimationFrame(() => { ov.classList.add('open'); inp.focus(); });
}
function closePalette() {
if (!_paletteOpen) return;
_paletteOpen = false;
const ov = document.getElementById('cmdPalette');
if (!ov) return;
ov.classList.remove('open');
setTimeout(() => { ov.style.display = 'none'; }, 180);
}
function renderPaletteItems(q) {
const list = document.getElementById('cmdPaletteList');
if (!list) return;
const low = q.toLowerCase().trim();
const filtered = low
? _PALETTE_COMMANDS.filter(c => c.label.toLowerCase().includes(low) || c.group.toLowerCase().includes(low))
: _PALETTE_COMMANDS;
let currentGroup = null;
list.innerHTML = '';
filtered.forEach((cmd, i) => {
if (cmd.group !== currentGroup) {
currentGroup = cmd.group;
const g = document.createElement('div');
g.className = 'cp-group';
g.textContent = cmd.group;
list.appendChild(g);
}
const row = document.createElement('div');
row.className = 'cp-item' + (i === 0 ? ' cp-active' : '');
row.dataset.q = cmd.q;
row.dataset.search = cmd.search ? '1' : '';
const lbl = cmd.label.replace(new RegExp(low.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'), 'gi'),
m => `<mark>${m}</mark>`);
row.innerHTML = `<span class="cp-icon">◈</span><span class="cp-label">${lbl}</span><kbd class="cp-kbd">↵</kbd>`;
row.addEventListener('click', () => firePaletteItem(row));
list.appendChild(row);
});
}
function movePaletteSelection(dir) {
const items = Array.from(document.querySelectorAll('#cmdPaletteList .cp-item'));
if (!items.length) return;
const cur = items.findIndex(el => el.classList.contains('cp-active'));
const next = (cur + dir + items.length) % items.length;
items.forEach(el => el.classList.remove('cp-active'));
items[next].classList.add('cp-active');
items[next].scrollIntoView({ block: 'nearest' });
}
function firePaletteItem(el) {
if (!el) {
const active = document.querySelector('#cmdPaletteList .cp-active');
if (!active) return;
el = active;
}
const q = el.dataset.q;
const isSearch = el.dataset.search === '1';
closePalette();
if (isSearch) {
if (typeof openSearchModal === 'function') openSearchModal();
return;
}
if (q) {
document.getElementById('textInput').value = q;
sendMessage();
}
}
// Keyboard events
document.addEventListener('keydown', e => {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
_paletteOpen ? closePalette() : openPalette();
return;
}
if (!_paletteOpen) return;
if (e.key === 'Escape') { e.preventDefault(); closePalette(); }
if (e.key === 'ArrowDown') { e.preventDefault(); movePaletteSelection(1); }
if (e.key === 'ArrowUp') { e.preventDefault(); movePaletteSelection(-1); }
if (e.key === 'Enter') { e.preventDefault(); firePaletteItem(null); }
});
// Filter on type
document.getElementById('cmdPaletteInput')?.addEventListener('input', e => {
renderPaletteItems(e.target.value);
});
// Close on backdrop click
document.getElementById('cmdPalette')?.addEventListener('click', e => {
if (e.target.id === 'cmdPalette') closePalette();
});