177 lines
6.0 KiB
JavaScript
177 lines
6.0 KiB
JavaScript
/**
|
||
* 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;
|
||
|
||
if (state.get('sseReconnecting')) {
|
||
serverLed.className = 'led led--amber led--pulse';
|
||
serverLed.querySelector('.led__label').textContent = 'Reconnecting…';
|
||
} else if (serverOnline) {
|
||
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');
|
||
const serverTypeBadge = document.getElementById('server-type-badge');
|
||
const versionBadge = document.getElementById('app-version-badge');
|
||
if (!el || !status) return;
|
||
|
||
const agent = status.agent ?? {};
|
||
const queue = {
|
||
pending_count: status.queue?.pending_count ?? 0,
|
||
consumed_count: status.queue?.consumed_count ?? 0,
|
||
};
|
||
const serverType = status.server?.type || 'unknown';
|
||
const version = status.server?.version || '–';
|
||
|
||
if (serverTypeBadge) {
|
||
serverTypeBadge.textContent = escapeHtml(serverType);
|
||
}
|
||
|
||
if (versionBadge) {
|
||
versionBadge.textContent = `v${version}`;
|
||
}
|
||
|
||
el.innerHTML = `
|
||
<div class="stat-row">
|
||
<span class="stat-label">Server Up</span>
|
||
<span class="stat-value">${fmtTime(status.server?.started_at)}</span>
|
||
</div>
|
||
<div class="stat-row">
|
||
<span class="stat-label">Version</span>
|
||
<span class="stat-value">v${escapeHtml(version)}</span>
|
||
</div>
|
||
<div class="stat-row">
|
||
<span class="stat-label">Server Type</span>
|
||
<span class="stat-value">${escapeHtml(serverType)}</span>
|
||
</div>
|
||
<div class="stat-row">
|
||
<span class="stat-label">Pending</span>
|
||
<span class="stat-value stat-value--cyan">${queue.pending_count}</span>
|
||
</div>
|
||
<div class="stat-row">
|
||
<span class="stat-label">Consumed</span>
|
||
<span class="stat-value stat-value--amber">${queue.consumed_count}</span>
|
||
</div>
|
||
<div class="stat-row">
|
||
<span class="stat-label">Agent</span>
|
||
<span class="stat-value">${agent.agent_id ? escapeHtml(agent.agent_id) : '–'}</span>
|
||
</div>
|
||
<div class="stat-row">
|
||
<span class="stat-label">Last Seen</span>
|
||
<span class="stat-value" data-ts-rel="${agent.last_seen_at || ''}">${fmtRelative(agent.last_seen_at)}</span>
|
||
</div>
|
||
<div class="stat-row">
|
||
<span class="stat-label">Last Fetch</span>
|
||
<span class="stat-value" data-ts-rel="${agent.last_fetch_at || ''}">${fmtRelative(agent.last_fetch_at)}</span>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
/** 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 staleInput = document.getElementById('cfg-stale');
|
||
const saveBtn = document.getElementById('cfg-save');
|
||
|
||
// Populate from state
|
||
state.subscribe('config', (cfg) => {
|
||
if (!cfg) return;
|
||
staleInput.value = cfg.agent_stale_after_seconds;
|
||
});
|
||
|
||
form.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
saveBtn.disabled = true;
|
||
const original = saveBtn.innerHTML;
|
||
saveBtn.innerHTML = '<span class="spinner"></span>';
|
||
|
||
try {
|
||
const cfg = await api.updateConfig({
|
||
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, '<')
|
||
.replace(/>/g, '>');
|
||
}
|
||
|