init
This commit is contained in:
108
static/js/app.js
Normal file
108
static/js/app.js
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user