Files
flynn/src/gateway/ui/pages/usage.js
T
William Valentin 6090508bad style: auto-fix ESLint issues (curly braces and formatting)
- 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)
2026-02-11 10:30:24 -08:00

171 lines
4.6 KiB
JavaScript

/**
* Flynn Token Usage Page
*
* Shows per-session token usage breakdown including input/output tokens,
* API calls, estimated cost, and delegation details.
* Auto-refreshes every 30 seconds.
*/
let _timer = null;
function formatNumber(n) {
return (n ?? 0).toLocaleString();
}
function formatCost(n) {
if (!n || n === 0) {return '$0.00';}
if (n < 0.01) {return `$${n.toFixed(4)}`;}
return `$${n.toFixed(2)}`;
}
function truncateId(id) {
if (!id) {return '-';}
if (id.length <= 24) {return id;}
return id.slice(0, 24) + '\u2026';
}
async function loadUsage(el, client) {
let data;
try {
data = await client.call('system.tokenUsage');
} catch (err) {
el.innerHTML = `<div class="empty-state">Failed to load usage: ${err.message}</div>`;
return;
}
const sessions = data?.sessions ?? [];
// Compute totals across all sessions
let totalInput = 0;
let totalOutput = 0;
let totalCalls = 0;
let totalCost = 0;
for (const s of sessions) {
totalInput += s.total?.inputTokens ?? 0;
totalOutput += s.total?.outputTokens ?? 0;
totalCalls += s.total?.calls ?? 0;
totalCost += s.total?.estimatedCost ?? 0;
}
// Summary cards
const summaryHtml = `
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">Total Input Tokens</div>
<div class="stat-value">${formatNumber(totalInput)}</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Output Tokens</div>
<div class="stat-value">${formatNumber(totalOutput)}</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Tokens</div>
<div class="stat-value">${formatNumber(totalInput + totalOutput)}</div>
</div>
<div class="stat-card">
<div class="stat-label">API Calls</div>
<div class="stat-value">${formatNumber(totalCalls)}</div>
</div>
<div class="stat-card">
<div class="stat-label">Estimated Cost</div>
<div class="stat-value">${formatCost(totalCost)}</div>
</div>
<div class="stat-card">
<div class="stat-label">Active Sessions</div>
<div class="stat-value">${sessions.length}</div>
</div>
</div>
`;
// Per-session table
let tableHtml = '';
if (sessions.length === 0) {
tableHtml = '<div class="empty-state">No active sessions with usage data</div>';
} else {
const rows = sessions.map(s => {
const inTok = s.total?.inputTokens ?? 0;
const outTok = s.total?.outputTokens ?? 0;
const calls = s.total?.calls ?? 0;
const cost = s.total?.estimatedCost ?? 0;
// Build delegation breakdown if present
const delegationEntries = Object.entries(s.delegation ?? {});
let delegationCell = '<span class="text-muted">-</span>';
if (delegationEntries.length > 0) {
delegationCell = delegationEntries.map(([tier, stats]) =>
`<span class="badge ok">${tier}</span> ${formatNumber(stats.inputTokens)}/${formatNumber(stats.outputTokens)}`,
).join('<br>');
}
return `
<tr>
<td title="${s.sessionId}">${truncateId(s.sessionId)}</td>
<td>${formatNumber(inTok)}</td>
<td>${formatNumber(outTok)}</td>
<td>${formatNumber(inTok + outTok)}</td>
<td>${formatNumber(calls)}</td>
<td>${formatCost(cost)}</td>
<td>${delegationCell}</td>
</tr>
`;
}).join('');
tableHtml = `
<table>
<thead>
<tr>
<th>Session</th>
<th>Input</th>
<th>Output</th>
<th>Total</th>
<th>Calls</th>
<th>Cost</th>
<th>Delegation</th>
</tr>
</thead>
<tbody>
${rows}
</tbody>
</table>
`;
}
el.innerHTML = `
<div class="usage-header">
<h1 class="page-title">Token Usage</h1>
<button class="btn btn-secondary" id="usage-refresh-btn">Refresh</button>
</div>
${summaryHtml}
<h2 class="section-title">Per-Session Breakdown</h2>
${tableHtml}
`;
// Wire up refresh button
const refreshBtn = el.querySelector('#usage-refresh-btn');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
loadUsage(el, client).catch(() => {});
});
}
}
export const UsagePage = {
async render(el, client) {
await loadUsage(el, client);
// Auto-refresh every 30 seconds
_timer = setInterval(() => {
loadUsage(el, client).catch(() => {});
}, 30000);
},
teardown() {
if (_timer) {
clearInterval(_timer);
_timer = null;
}
},
};