Changes: - Add DEFAULT_WAIT_SECONDS = 50 (replaces config.default_wait_seconds) - Add DEFAULT_EMPTY_RESPONSE = 'call this tool... ' (replaces config.default_empty_response) - Remove wait and empty response fields from WebUI Settings panel (only agent_stale_after_seconds remains configurable) Rationale: These values are fundamental to the 60s timeout mitigation strategy and should not be user-configurable. The 50s wait with 5s keepalives is the optimal balance for staying under the Copilot 60s wall-clock limit while providing responsive progress feedback.
152 lines
5.1 KiB
JavaScript
152 lines
5.1 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;
|
||
|
||
// 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 = `
|
||
<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">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, '>');
|
||
}
|
||
|