/* ═══════════════════════════════════════════════ mavweb — shared JS for Ethos workstation shell Command palette, search, keyboard shortcuts, inspector, connection status ═══════════════════════════════════════════════ */ (function() { 'use strict'; // ── Keyboard Shortcuts ── const shortcuts = { 'ctrl+k': 'openPalette', 'ctrl+/': 'openSearch', 'escape': 'closeAll', 'alt+ArrowLeft': 'navigateBack', 'alt+ArrowRight': 'navigateForward', }; document.addEventListener('keydown', function(e) { const key = []; if (e.ctrlKey || e.metaKey) key.push('ctrl'); if (e.altKey) key.push('alt'); if (e.shiftKey) key.push('shift'); key.push(e.key === ' ' ? 'Space' : e.key); const combo = key.join('+').toLowerCase(); const action = shortcuts[combo]; if (action === 'openPalette') { e.preventDefault(); openPalette(); } else if (action === 'openSearch') { e.preventDefault(); openSearch(); } else if (action === 'closeAll') { closePalette(); closeSearch(); closeInspector(); } else if (action === 'navigateBack') { history.back(); } else if (action === 'navigateForward') { history.forward(); } }); // ── Command Palette ── let paletteEl = null; let paletteInput = null; // Page entries for the command palette — uses ethos-icons.svg sprite const palettePages = [ { label: 'Dashboard', url: '/dash', icon: '' }, { label: 'History', url: '/history', icon: '' }, { label: 'Rule Trace', url: '/trace', icon: '' }, { label: 'Notifications', url: '/notifications', icon: '' }, { label: 'Voice', url: '/', icon: '' }, { label: 'Tools', url: '/tools', icon: '' }, { label: 'Passkey', url: '/auth/passkey', icon: '' }, ]; function buildPalette() { if (paletteEl) return; paletteEl = document.createElement('div'); paletteEl.className = 'cmd-palette-overlay'; paletteEl.innerHTML = '
' + '
' + '' + '' + '
' + '
' + '
'; document.body.appendChild(paletteEl); paletteInput = paletteEl.querySelector('input'); paletteEl.addEventListener('click', function(e) { if (e.target === paletteEl) closePalette(); }); paletteInput.addEventListener('keydown', function(e) { if (e.key === 'Escape') { closePalette(); return; } if (e.key === 'Enter') { const selected = paletteEl.querySelector('.cmd-palette-item.selected'); if (selected) { e.preventDefault(); closePalette(); window.location.href = selected.getAttribute('data-url') || selected.getAttribute('href'); } return; } if (e.key === 'ArrowDown') { e.preventDefault(); moveSelection(1); return; } if (e.key === 'ArrowUp') { e.preventDefault(); moveSelection(-1); return; } }); paletteInput.addEventListener('input', filterPalette); } function moveSelection(dir) { const items = paletteEl.querySelectorAll('.cmd-palette-item'); if (!items.length) return; let idx = -1; for (let i = 0; i < items.length; i++) { if (items[i].classList.contains('selected')) { idx = i; items[i].classList.remove('selected'); break; } } let next = idx === -1 ? 0 : (idx + dir + items.length) % items.length; items[next].classList.add('selected'); items[next].scrollIntoView({ block: 'nearest' }); } function filterPalette() { const q = paletteInput.value.toLowerCase(); const list = paletteEl.querySelector('#paletteList'); let html = ''; const all = palettePages; for (let i = 0; i < all.length; i++) { const p = all[i]; if (q && !p.label.toLowerCase().includes(q)) continue; html += '' + '' + p.icon + '' + '' + p.label + '' + ''; } list.innerHTML = html; } window.__openPalette = function openPalette() { buildPalette(); closeSearch(); paletteEl.classList.add('open'); setTimeout(function() { paletteInput.focus(); paletteInput.select(); }, 50); filterPalette(); }; function closePalette() { if (paletteEl) paletteEl.classList.remove('open'); } window.closePalette = closePalette; // ── Search (reuses command palette with search context) ── window.__openSearch = function openSearch() { openPalette(); if (paletteInput) { paletteInput.placeholder = 'Search pages, tools, history…'; } }; function closeSearch() { // search uses the same palette overlay if (paletteInput) paletteInput.placeholder = 'Go to page or run action…'; } // ── Close all overlays ── function closeAll() { closePalette(); closeInspector(); } // ── Inspector ── window.openInspector = function openInspector(title, content) { var inspector = document.getElementById('inspector'); var titleEl = document.getElementById('inspectorTitle'); var bodyEl = document.getElementById('inspectorBody'); if (!inspector || !titleEl || !bodyEl) return; titleEl.textContent = title || 'Details'; bodyEl.innerHTML = content || ''; inspector.classList.add('open'); }; window.closeInspector = function closeInspector() { var inspector = document.getElementById('inspector'); if (inspector) inspector.classList.remove('open'); }; // ── Connection Status ── function updateConnStatus() { var dot = document.getElementById('connDot'); if (!dot) return; fetch('/api/ping') .then(function(r) { dot.className = 'dot ' + (r.ok ? 'online' : 'offline'); }) .catch(function() { dot.className = 'dot offline'; }); } // Check connection on load and every 60s updateConnStatus(); setInterval(updateConnStatus, 60000); // ── Inspector: row click to open detail ── // Attach click handler to tables for rows with data attributes document.addEventListener('click', function(e) { var row = e.target.closest('tr.clickable'); if (!row) return; var detailUrl = row.getAttribute('data-detail-url'); if (detailUrl) { fetch(detailUrl) .then(function(r) { return r.text(); }) .then(function(html) { openInspector(row.getAttribute('data-detail-title') || 'Details', html); }) .catch(function() {}); } }); })();