/** * static/js/status.js * Renders the server status and agent activity panels, * and the config settings panel. */ import { state } from './state.js'; import { api } from './api.js'; import { toast } from './app.js'; // ── Time helpers ────────────────────────────────────────────────────────── function fmtTime(isoStr) { if (!isoStr) return '–'; const d = new Date(isoStr); return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); } function fmtRelative(isoStr) { if (!isoStr) return '–'; const d = new Date(isoStr); const diff = Math.floor((Date.now() - d.getTime()) / 1000); if (diff < 5) return 'just now'; if (diff < 60) return `${diff}s ago`; if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; return `${Math.floor(diff / 3600)}h ago`; } // ── Server online indicator (header) ───────────────────────────────────── function updateHeaderLeds(serverOnline, status) { const serverLed = document.getElementById('led-server'); const agentLed = document.getElementById('led-agent'); if (!serverLed || !agentLed) return; // Don't overwrite reconnecting state – events.js sets that if (serverOnline && !state.get('sseReconnecting')) { serverLed.className = 'led led--green led--pulse'; serverLed.querySelector('.led__label').textContent = 'Server Online'; } else if (!serverOnline) { serverLed.className = 'led led--red'; serverLed.querySelector('.led__label').textContent = 'Server Offline'; } if (status?.agent?.connected) { agentLed.className = 'led led--cyan led--pulse'; agentLed.querySelector('.led__label').textContent = 'Agent Connected'; } else { agentLed.className = 'led led--muted'; agentLed.querySelector('.led__label').textContent = 'Agent Idle'; } } // ── Status sidebar panel ────────────────────────────────────────────────── function renderStatusPanel(status) { const el = document.getElementById('status-panel-body'); if (!el || !status) return; const agent = status.agent; const queue = status.queue; el.innerHTML = `
Server Up ${fmtTime(status.server?.started_at)}
Pending ${queue.pending_count}
Consumed ${queue.consumed_count}
Agent ${agent.agent_id ? escapeHtml(agent.agent_id) : '–'}
Last Seen ${fmtRelative(agent.last_seen_at)}
Last Fetch ${fmtRelative(agent.last_fetch_at)}
`; } /** Called by app.js on a timer to keep relative times fresh. */ export function refreshStatusTimestamps() { document.querySelectorAll('[data-ts-rel]').forEach(el => { const iso = el.dataset.tsRel; if (iso) el.textContent = fmtRelative(iso); }); } export function initStatus() { state.subscribe('serverOnline', (online) => { updateHeaderLeds(online, state.get('status')); }); state.subscribe('status', (status) => { updateHeaderLeds(state.get('serverOnline'), status); renderStatusPanel(status); }); } // ── Config panel ────────────────────────────────────────────────────────── export function initConfig() { const form = document.getElementById('config-form'); const waitInput = document.getElementById('cfg-wait'); const emptyInput = document.getElementById('cfg-empty'); const staleInput = document.getElementById('cfg-stale'); const saveBtn = document.getElementById('cfg-save'); // Populate from state state.subscribe('config', (cfg) => { if (!cfg) return; waitInput.value = cfg.default_wait_seconds; emptyInput.value = cfg.default_empty_response; staleInput.value = cfg.agent_stale_after_seconds; }); form.addEventListener('submit', async (e) => { e.preventDefault(); saveBtn.disabled = true; const original = saveBtn.innerHTML; saveBtn.innerHTML = ''; try { const cfg = await api.updateConfig({ default_wait_seconds: parseInt(waitInput.value, 10) || 10, default_empty_response: emptyInput.value, agent_stale_after_seconds: parseInt(staleInput.value, 10) || 30, }); state.set('config', cfg); toast('Settings saved', 'success'); } catch (e) { toast(e.message, 'error'); } finally { saveBtn.disabled = false; saveBtn.innerHTML = original; } }); } function escapeHtml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>'); }