// app.js — boot, router, theme, command palette wiring. // Each pane self-registers via registerPane(); only the active pane is initialized. import { jget } from './lib/api.js'; import { initPalette, registerCommand, setHealthPill, setBrandVer, setFooterBuild, ok, err } from './lib/ui.js'; // Pane registry: id -> { label, init(host), refresh?(host), inited:false } const PANES = new Map(); const MAIN = document.getElementById('main'); export function registerPane(id, def) { PANES.set(id, { ...def, inited: false }); registerCommand({ id: `go:${id}`, label: `Go to ${def.label}`, group: 'Navigate', keywords: `tab ${id} ${def.label}`, run: () => activate(id), }); } // dynamic imports so each pane is its own ES module const PANE_LOADERS = { dashboard: () => import('./panes/dashboard.js'), agent: () => import('./panes/agent.js'), inbox: () => import('./panes/inbox.js'), tools: () => import('./panes/tools.js'), items: () => import('./panes/items.js'), diffusion: () => import('./panes/diffusion.js'), demand: () => import('./panes/demand.js'), lang: () => import('./panes/lang.js'), slack: () => import('./panes/slack.js'), mac: () => import('./panes/mac.js'), files: () => import('./panes/files.js'), api: () => import('./panes/api.js'), websocket: () => import('./panes/websocket.js'), system: () => import('./panes/system.js'), about: () => import('./panes/about.js'), }; const NAV_LABELS = { dashboard: 'Dashboard', agent: 'Agent', inbox: 'Inbox', tools: 'Tools', items: 'Items', diffusion: 'Diffusion', demand: 'Demand', lang: 'Lang', slack: 'Slack', mac: 'macOS', files: 'Files · Platform', api: 'API Explorer', websocket: 'WebSocket', system: 'System', about: 'About', }; async function activate(id) { if (!PANE_LOADERS[id]) id = 'dashboard'; // mark nav document.querySelectorAll('.nav-item').forEach(el => { el.classList.toggle('active', el.dataset.tab === id); }); // mark pane document.querySelectorAll('.pane').forEach(el => { el.classList.toggle('active', el.dataset.pane === id); }); const crumb = document.getElementById('crumb-tab'); if (crumb) crumb.textContent = NAV_LABELS[id] || id; document.title = `${NAV_LABELS[id] || id} · CxWebApp`; if (location.hash !== `#${id}`) history.replaceState(null, '', `#${id}`); // lazy-load pane module if (!PANES.has(id)) { try { await PANE_LOADERS[id](); } catch (e) { console.error(`pane ${id} failed to load`, e); const host = document.querySelector(`[data-pane="${id}"]`); if (host) host.innerHTML = `
Failed to load ${id}: ${e.message}
`; return; } } const pane = PANES.get(id); if (!pane) return; const host = document.querySelector(`[data-pane="${id}"]`); if (!host) return; if (!pane.inited) { try { await pane.init(host); pane.inited = true; } catch (e) { err(`init ${id}: ${e.message}`); console.error(e); } } else if (pane.refresh) { try { await pane.refresh(host); } catch (e) { console.warn(`refresh ${id}`, e); } } } // ---------------- theme ---------------- const themeBtn = document.getElementById('theme-toggle'); themeBtn?.addEventListener('click', () => { const dark = document.documentElement.classList.toggle('dark'); localStorage.setItem('cx_theme', dark ? 'dark' : 'light'); }); // ---------------- nav ---------------- document.querySelectorAll('.nav-item').forEach(el => { el.addEventListener('click', () => activate(el.dataset.tab)); }); window.addEventListener('hashchange', () => { const id = (location.hash || '').replace('#', ''); if (id) activate(id); }); // ---------------- header health + version ---------------- async function refreshHealth() { try { const j = await jget('/api/health'); setHealthPill('ok', 'healthy'); return j; } catch (e) { setHealthPill('err', 'down'); } } async function refreshVersion() { try { const j = await jget('/api/version'); setBrandVer(`${j.name || 'cxwebapp'} ${j.version || ''}`.trim()); setFooterBuild(`${(j.git_sha || '').slice(0, 7) || '—'} · ${j.build_time || ''}`.trim()); } catch { setBrandVer('—'); } } // ---------------- palette commands (global) ---------------- function registerGlobalCommands() { registerCommand({ id: 'theme:toggle', label: 'Toggle theme', group: 'View', run: () => themeBtn?.click() }); registerCommand({ id: 'reload', label: 'Reload page', group: 'View', run: () => location.reload() }); registerCommand({ id: 'open:health', label: 'Open /api/health', group: 'API', run: () => window.open('/api/health', '_blank') }); registerCommand({ id: 'open:version', label: 'Open /api/version', group: 'API', run: () => window.open('/api/version', '_blank') }); registerCommand({ id: 'open:system', label: 'Open /api/system', group: 'API', run: () => window.open('/api/system', '_blank') }); } // ---------------- boot ---------------- (async function boot() { initPalette(); registerGlobalCommands(); const initial = (location.hash || '#dashboard').replace('#', ''); await activate(initial); refreshHealth(); refreshVersion(); setInterval(refreshHealth, 10_000); // greet once ok('Control center ready', 'CxWebApp'); })();