/** * Flynn Live Ops Dashboard * * Shows core counters, model performance, event stream, active requests, * and channel status. Fast metrics refresh every 3s, slow health every 10s. */ let _fastTimer = null; let _slowTimer = null; function formatUptime(seconds) { const d = Math.floor(seconds / 86400); const h = Math.floor((seconds % 86400) / 3600); const m = Math.floor((seconds % 3600) / 60); const s = seconds % 60; const parts = []; if (d > 0) {parts.push(`${d}d`);} if (h > 0) {parts.push(`${h}h`);} if (m > 0) {parts.push(`${m}m`);} parts.push(`${s}s`); return parts.join(' '); } function timeAgo(timestamp) { const secs = Math.floor((Date.now() - timestamp) / 1000); if (secs < 60) {return `${secs}s ago`;} if (secs < 3600) {return `${Math.floor(secs / 60)}m ago`;} return `${Math.floor(secs / 3600)}h ago`; } function formatTime(timestamp) { const d = new Date(timestamp); return d.toLocaleTimeString('en-GB', { hour12: false }); } function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } // ── Initial full render ───────────────────────────────────────── function renderSkeleton(el) { el.innerHTML = `

Live Ops Dashboard

Core Counters

Loading...

Model Performance

Loading...

Event Stream

Loading events...

Active Requests

Loading...

Channels

Loading...
`; } // ── Section updaters (targeted DOM updates) ───────────────────── function updateCounters(metrics, health) { const el = document.getElementById('ops-counters'); if (!el) {return;} const sessions = health?.sessions ?? 0; const errCount = metrics?.errors ?? 0; const cards = [ { label: 'Messages Processed', value: String(metrics?.messagesProcessed ?? 0), cls: '' }, { label: 'Active Sessions', value: String(sessions), cls: '' }, { label: 'Queue Depth', value: String(metrics?.queueDepth ?? 0), cls: '' }, { label: 'Uptime', value: formatUptime(metrics?.uptime ?? 0), cls: '' }, { label: 'Active Requests', value: String(metrics?.activeRequests ?? 0), cls: '' }, { label: 'Errors', value: String(errCount), cls: errCount > 0 ? 'error' : '' }, ]; el.innerHTML = cards.map(c => `
${c.label}
${c.value}
`, ).join(''); } function updateModelTable(metrics) { const el = document.getElementById('ops-model-table'); if (!el) {return;} const mc = metrics?.modelCalls; const calls = mc?.recentCalls ?? []; if (calls.length === 0) { el.innerHTML = '
No model calls recorded yet
'; return; } const totalCalls = mc.total ?? 0; const avgLatency = mc.avgLatency ?? 0; const errorRate = mc.errorRate ?? 0; const summaryHtml = `
Total Calls: ${totalCalls}
Avg Latency: ${avgLatency}ms
Error Rate: ${(errorRate * 100).toFixed(2)}%
`; // Show newest first const rows = [...calls].reverse().map(c => { const status = c.error ? '' : ''; return ` ${timeAgo(c.timestamp)} ${escapeHtml(c.provider)} ${c.latency}ms ${c.tokensPerSec.toFixed(1)} ${c.inputTokens}/${c.outputTokens} ${status} `; }).join(''); el.innerHTML = ` ${summaryHtml} ${rows}
Time Provider Latency Tokens/sec In/Out Status
`; } function updateEvents(eventsData) { const el = document.getElementById('ops-events'); if (!el) {return;} const events = eventsData?.events ?? []; if (events.length === 0) { el.innerHTML = '
No events recorded yet
'; return; } // Events come newest-first from the API; show newest at bottom for log feel const reversed = [...events].reverse(); el.innerHTML = reversed.map(e => { const time = formatTime(e.timestamp); const level = (e.level || 'info').toUpperCase(); const cls = `event-level-${e.level || 'info'}`; return `
[${time}] [${level}] ${escapeHtml(e.source)}: ${escapeHtml(e.message)}
`; }).join(''); // Auto-scroll to bottom el.scrollTop = el.scrollHeight; } function updateActiveRequests(requestsData) { const el = document.getElementById('ops-requests'); if (!el) {return;} const requests = requestsData?.requests ?? []; if (requests.length === 0) { el.innerHTML = '
No active requests
'; return; } const rows = requests.map(r => { const duration = r.durationMs < 1000 ? `${r.durationMs}ms` : `${(r.durationMs / 1000).toFixed(1)}s`; const started = formatTime(r.startedAt); return ` ${escapeHtml(r.sessionId)} ${escapeHtml(r.channel)} ${duration} ${started} `; }).join(''); el.innerHTML = ` ${rows}
Session Channel Duration Started
`; } function updateChannels(channelsData) { const el = document.getElementById('ops-channels'); if (!el) {return;} const channels = channelsData?.channels ?? []; if (channels.length === 0) { el.innerHTML = '
No channels registered
'; return; } el.innerHTML = channels.map(ch => `
${escapeHtml(ch.name)}
`, ).join(''); } // ── Data fetching ─────────────────────────────────────────────── async function fetchFast(client) { try { const [metrics, eventsData, requestsData] = await Promise.all([ client.call('system.metrics'), client.call('system.events', { limit: 50 }), client.call('system.activeRequests'), ]); return { metrics, eventsData, requestsData }; } catch { return null; } } async function fetchSlow(client) { try { const [health, channels] = await Promise.all([ client.call('system.health'), client.call('system.channels'), ]); return { health, channels }; } catch { return null; } } // ── Main load function ────────────────────────────────────────── let _lastHealth = null; let _lastMetrics = null; async function loadDashboard(el, client) { renderSkeleton(el); // Fetch everything initially const [fast, slow] = await Promise.all([ fetchFast(client), fetchSlow(client), ]); _lastHealth = slow?.health ?? null; _lastMetrics = fast?.metrics ?? null; if (fast) { updateCounters(fast.metrics, _lastHealth); updateModelTable(fast.metrics); updateEvents(fast.eventsData); updateActiveRequests(fast.requestsData); } if (slow) { updateChannels(slow.channels); } // Fast refresh: 3 seconds for metrics, events, requests _fastTimer = setInterval(async () => { const data = await fetchFast(client); if (data) { _lastMetrics = data.metrics; updateCounters(data.metrics, _lastHealth); updateModelTable(data.metrics); updateEvents(data.eventsData); updateActiveRequests(data.requestsData); } }, 3000); // Slow refresh: 10 seconds for health, channels _slowTimer = setInterval(async () => { const data = await fetchSlow(client); if (data) { _lastHealth = data.health; updateCounters(_lastMetrics, _lastHealth); updateChannels(data.channels); } }, 10000); } export const DashboardPage = { async render(el, client) { await loadDashboard(el, client); }, teardown() { if (_fastTimer) { clearInterval(_fastTimer); _fastTimer = null; } if (_slowTimer) { clearInterval(_slowTimer); _slowTimer = null; } _lastHealth = null; _lastMetrics = null; }, };