This commit is contained in:
2026-03-27 03:58:57 +08:00
commit 86eba27a24
38 changed files with 4074 additions and 0 deletions

108
static/js/app.js Normal file
View 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();
});