6090508bad
- Add curly braces to all if/else/for/while statements - Fix indentation and trailing spaces - Auto-fixed 372 linting errors using eslint --fix - Remaining issues are warnings only (non-null assertions, explicit any types)
334 lines
9.4 KiB
JavaScript
334 lines
9.4 KiB
JavaScript
/**
|
|
* 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 = `
|
|
<h1 class="page-title">Live Ops Dashboard</h1>
|
|
|
|
<h2 class="section-title">Core Counters</h2>
|
|
<div class="stats-grid" id="ops-counters">
|
|
<div class="stat-card"><div class="stat-label">Loading...</div><div class="stat-value">—</div></div>
|
|
</div>
|
|
|
|
<h2 class="section-title">Model Performance</h2>
|
|
<div id="ops-model-table">
|
|
<div class="text-muted text-sm">Loading...</div>
|
|
</div>
|
|
|
|
<h2 class="section-title">Event Stream</h2>
|
|
<div class="event-stream" id="ops-events">
|
|
<div class="event-row event-level-info">Loading events...</div>
|
|
</div>
|
|
|
|
<h2 class="section-title">Active Requests</h2>
|
|
<div id="ops-requests">
|
|
<div class="text-muted text-sm">Loading...</div>
|
|
</div>
|
|
|
|
<h2 class="section-title">Channels</h2>
|
|
<div id="ops-channels" class="channels-grid">
|
|
<div class="text-muted text-sm">Loading...</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// ── 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 =>
|
|
`<div class="stat-card">
|
|
<div class="stat-label">${c.label}</div>
|
|
<div class="stat-value ${c.cls}">${c.value}</div>
|
|
</div>`,
|
|
).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 = '<div class="text-muted text-sm">No model calls recorded yet</div>';
|
|
return;
|
|
}
|
|
|
|
const totalCalls = mc.total ?? 0;
|
|
const avgLatency = mc.avgLatency ?? 0;
|
|
const errorRate = mc.errorRate ?? 0;
|
|
|
|
const summaryHtml = `
|
|
<div class="metrics-summary">
|
|
<div class="metric"><span>Total Calls:</span> <span class="metric-value">${totalCalls}</span></div>
|
|
<div class="metric"><span>Avg Latency:</span> <span class="metric-value">${avgLatency}ms</span></div>
|
|
<div class="metric"><span>Error Rate:</span> <span class="metric-value">${(errorRate * 100).toFixed(2)}%</span></div>
|
|
</div>
|
|
`;
|
|
|
|
// Show newest first
|
|
const rows = [...calls].reverse().map(c => {
|
|
const status = c.error ? '<span class="text-error">✗</span>' : '<span class="text-success">✓</span>';
|
|
return `<tr>
|
|
<td>${timeAgo(c.timestamp)}</td>
|
|
<td>${escapeHtml(c.provider)}</td>
|
|
<td>${c.latency}ms</td>
|
|
<td>${c.tokensPerSec.toFixed(1)}</td>
|
|
<td>${c.inputTokens}/${c.outputTokens}</td>
|
|
<td>${status}</td>
|
|
</tr>`;
|
|
}).join('');
|
|
|
|
el.innerHTML = `
|
|
${summaryHtml}
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Time</th>
|
|
<th>Provider</th>
|
|
<th>Latency</th>
|
|
<th>Tokens/sec</th>
|
|
<th>In/Out</th>
|
|
<th>Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>${rows}</tbody>
|
|
</table>
|
|
`;
|
|
}
|
|
|
|
function updateEvents(eventsData) {
|
|
const el = document.getElementById('ops-events');
|
|
if (!el) {return;}
|
|
|
|
const events = eventsData?.events ?? [];
|
|
|
|
if (events.length === 0) {
|
|
el.innerHTML = '<div class="event-row event-level-info">No events recorded yet</div>';
|
|
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 `<div class="event-row ${cls}">[${time}] [${level}] ${escapeHtml(e.source)}: ${escapeHtml(e.message)}</div>`;
|
|
}).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 = '<div class="text-muted text-sm">No active requests</div>';
|
|
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 `<tr>
|
|
<td>${escapeHtml(r.sessionId)}</td>
|
|
<td>${escapeHtml(r.channel)}</td>
|
|
<td>${duration}</td>
|
|
<td>${started}</td>
|
|
</tr>`;
|
|
}).join('');
|
|
|
|
el.innerHTML = `
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Session</th>
|
|
<th>Channel</th>
|
|
<th>Duration</th>
|
|
<th>Started</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>${rows}</tbody>
|
|
</table>
|
|
`;
|
|
}
|
|
|
|
function updateChannels(channelsData) {
|
|
const el = document.getElementById('ops-channels');
|
|
if (!el) {return;}
|
|
|
|
const channels = channelsData?.channels ?? [];
|
|
|
|
if (channels.length === 0) {
|
|
el.innerHTML = '<div class="text-muted text-sm">No channels registered</div>';
|
|
return;
|
|
}
|
|
|
|
el.innerHTML = channels.map(ch =>
|
|
`<div class="channel-card">
|
|
<span class="channel-dot ${ch.status}"></span>
|
|
<span class="channel-name">${escapeHtml(ch.name)}</span>
|
|
</div>`,
|
|
).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;
|
|
},
|
|
};
|