/**
* 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, '>');
}