109 lines
4.0 KiB
JavaScript
109 lines
4.0 KiB
JavaScript
/**
|
||
* 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();
|
||
});
|
||
|