/** * static/js/app.js * Application bootstrap – initialises all modules, kicks off data fetching, * and exports shared utilities like `toast`. */ import { api } from './api.js'; import { state } from './state.js'; import { connectSSE } from './events.js'; import { initInstructions, initComposer, refreshTimestamps } from './instructions.js'; import { initStatus, initConfig, refreshStatusTimestamps } from './status.js'; import { initTheme } from './theme.js'; // ── Toast notification ──────────────────────────────────────────────────── const _toastContainer = document.getElementById('toast-container'); export function toast(message, type = 'info') { const el = document.createElement('div'); el.className = `toast toast--${type}`; el.textContent = message; _toastContainer.appendChild(el); setTimeout(() => { el.style.animation = 'toast-out 240ms cubic-bezier(0.4,0,1,1) forwards'; el.addEventListener('animationend', () => el.remove()); }, 3000); } // ── Document title badge ────────────────────────────────────────────────── function updateTitle(instructions) { const pending = (instructions || []).filter(i => i.status === 'pending').length; document.title = pending > 0 ? `(${pending}) local-mcp` : 'local-mcp'; } // ── Reconnecting indicator ──────────────────────────────────────────────── function initReconnectingIndicator() { const serverLed = document.getElementById('led-server'); state.subscribe('sseReconnecting', (reconnecting) => { if (!serverLed) return; if (reconnecting) { serverLed.className = 'led led--amber led--pulse'; serverLed.querySelector('.led__label').textContent = 'Reconnecting…'; } // The full status update (onopen) will re-set the correct class }); } // ── Startup data load ───────────────────────────────────────────────────── async function loadInitialData() { try { const [instructions, status, config] = await Promise.all([ api.listInstructions('all'), api.status(), api.getConfig(), ]); state.set('instructions', instructions.items); state.set('status', status); state.set('config', config); state.set('serverOnline', true); state.set('sseReconnecting', false); } catch (err) { console.error('Failed to load initial data:', err); state.set('serverOnline', false); } } // ── Periodic refresh (fallback for SSE gaps) ────────────────────────────── function startPolling() { setInterval(async () => { try { const s = await api.status(); state.set('status', s); state.set('serverOnline', true); } catch { state.set('serverOnline', false); } }, 15_000); // Refresh relative timestamps every 20 seconds setInterval(() => { refreshTimestamps(); refreshStatusTimestamps(); }, 20_000); } // ── Subscribe to state changes ──────────────────────────────────────────── function initGlobalSubscriptions() { state.subscribe('instructions', updateTitle); } // ── Init ────────────────────────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', async () => { initTheme(); // must run first – sets button icon initReconnectingIndicator(); initStatus(); initConfig(); initInstructions(); initComposer(); initGlobalSubscriptions(); await loadInitialData(); connectSSE(); startPolling(); });