// 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');
})();