Files
local-mcp/static/js/status.js
Brandon Zhang 4db402f258 feat: hardcode wait time and default response as constants
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.
2026-03-27 15:45:06 +08:00

152 lines
5.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}