import { app, isRouteCurrent } from '../router.js'; import { api } from '../api.js'; import { escapeHTML, formatTokenCount, formatCost } from '../utils.js'; import { barTrack, barRankList, metricStrip, chartHeader } from '../components.js'; /* global uPlot */ let usageChart = null; let usageChartMode = 'activity'; let usageResizeObserver = null; let _usageSeries = []; export function cleanup() { if (usageChart) { usageChart.destroy(); usageChart = null; } if (usageResizeObserver) { usageResizeObserver.disconnect(); usageResizeObserver = null; } _usageSeries = []; } function buildChartData(series, mode) { if (!series || series.length === 0) return null; const ts = series.map(b => Math.floor(new Date(b.ts).getTime() / 1000)); if (mode === 'tokens') { return [ts, series.map(b => b.input_tokens || 0), series.map(b => b.output_tokens || 0)]; } if (mode === 'cost') { return [ts, series.map(b => b.cost || 0)]; } return [ts, series.map(b => b.runs || 0), series.map(b => b.tools || 0), series.map(b => b.errors || 0)]; } function renderChart(series, mode) { const container = document.getElementById('usage-chart'); if (!container) return; if (usageChart) { usageChart.destroy(); usageChart = null; } container.innerHTML = ''; const data = buildChartData(series, mode); if (!data) { container.innerHTML = '

No data for this period

'; return; } const width = container.clientWidth || 580; const height = 160; const axisStyle = { stroke: '#4e6070', grid: { stroke: 'rgba(28,38,55,0.6)', width: 1 }, ticks: { stroke: 'rgba(28,38,55,0.6)', width: 1 }, font: '11px Fira Code', }; let seriesDef; if (mode === 'tokens') { seriesDef = [ {}, { label: 'Input', stroke: '#22d3ee', width: 1.5, fill: 'rgba(34,211,238,0.08)', points: { show: false } }, { label: 'Output', stroke: '#34d399', width: 1.5, fill: 'rgba(52,211,153,0.08)', points: { show: false } }, ]; } else if (mode === 'cost') { seriesDef = [ {}, { label: 'Cost', stroke: '#fbbf24', width: 1.75, fill: 'rgba(251,191,36,0.1)', points: { show: false } }, ]; } else { seriesDef = [ {}, { label: 'Runs', stroke: '#34d399', width: 1.5, fill: 'rgba(52,211,153,0.08)', points: { show: false } }, { label: 'Tools', stroke: '#22d3ee', width: 1.5, fill: 'rgba(34,211,238,0.08)', points: { show: false } }, { label: 'Errors', stroke: '#f87171', width: 1.5, fill: 'rgba(248,113,113,0.08)', points: { show: false } }, ]; } usageChart = new window.uPlot({ width, height, cursor: { show: true }, scales: { x: { time: true }, y: { auto: true, min: 0 } }, axes: [{ ...axisStyle }, { ...axisStyle, size: 52 }], series: seriesDef, }, data, container); if (usageResizeObserver) usageResizeObserver.disconnect(); usageResizeObserver = new ResizeObserver(entries => { for (const entry of entries) { if (usageChart) usageChart.setSize({ width: entry.contentRect.width, height }); } }); usageResizeObserver.observe(container); } function renderFrameworkBreakdown(byFw) { const el = document.getElementById('usage-fw-breakdown'); if (!el) return; const entries = Object.entries(byFw || {}).sort((a, b) => { return (b[1].runs + b[1].tools + b[1].errors) - (a[1].runs + a[1].tools + a[1].errors); }); if (entries.length === 0) { el.innerHTML = '

No framework data

'; return; } const maxTotal = Math.max(...entries.map(([, s]) => s.runs + s.tools + s.errors), 1); el.innerHTML = entries.map(([name, stats]) => { const total = stats.runs + stats.tools + stats.errors; const cssClass = name.toLowerCase().replace(/[^a-z0-9-]/g, '-'); const active = (stats.active_sessions || 0) > 0; return `
${escapeHTML(name)} ${active ? `${stats.active_sessions} live` : ''}
runs${stats.runs || 0} tools${stats.tools || 0} ${(stats.errors || 0) > 0 ? `err${stats.errors}` : ''}
${barTrack({ value: total, max: maxTotal, fwClass: cssClass, size: 'sm' })}
`; }).join(''); } export async function renderUsage(routeToken) { app.innerHTML = `
Loading…
`; const [summary, toolsData, modelsData, tsData] = await Promise.all([ api('/v1/stats/summary').catch(() => null), api('/v1/stats/top-tools?limit=20').catch(() => ({ tools: [] })), api('/v1/stats/top-models?limit=10').catch(() => ({ models: [] })), api('/v1/stats/timeseries?window=7d').catch(() => ({ series: [] })), ]); if (routeToken && !isRouteCurrent(routeToken)) return; const tools = toolsData.tools || []; const models = modelsData.models || []; const s = summary || {}; _usageSeries = tsData.series || []; const t = _usageSeries.reduce((acc, b) => { acc.runs += b.runs || 0; acc.tools += b.tools || 0; acc.errors += b.errors || 0; acc.tokens += b.tokens || 0; acc.itok += b.input_tokens || 0; acc.otok += b.output_tokens || 0; acc.cost += b.cost || 0; return acc; }, { runs: 0, tools: 0, errors: 0, tokens: 0, itok: 0, otok: 0, cost: 0 }); const maxModel = models[0]?.count || 1; const maxTool = tools[0]?.count || 1; const content = document.getElementById('usage-content'); if (!content) return; content.innerHTML = `
Active Sessions
${s.active_sessions || 0}
Runs Today
${s.runs_today || 0}
Tool Calls Today
${s.tool_calls_today || 0}
Errors Today
${s.errors_today || 0}
Tokens Today
${formatTokenCount(s.tokens_today || 0)}
${(s.tokens_today || 0) > 0 ? `
${formatTokenCount(s.tokens_today * 0.7 || 0)} in · ${formatTokenCount(s.tokens_today * 0.3 || 0)} out
` : ''}
Cost Today
${formatCost(s.cost_today || 0)}
${(s.runs_today || 0) > 0 ? `
${formatCost((s.cost_today || 0) / s.runs_today)}/run
` : ''}
${chartHeader({ title: '7-Day Trend', controls: `
`, })} ${metricStrip([ { label: 'runs', value: String(t.runs), variant: 'total' }, { label: 'tools', value: String(t.tools), variant: 'total' }, { label: 'errors', value: String(t.errors), variant: 'total', alert: t.errors > 0 }, { label: 'tokens', value: formatTokenCount(t.tokens), variant: 'total' }, { label: 'cost', value: formatCost(t.cost), variant: 'total' }, ])}
Frameworks today
Top Models ${models.length}
${barRankList(models, { mapItem: m => ({ name: m.name, count: m.count, modifier: 'model' }), maxOverride: maxModel, emptyText: 'No model data', })}
Top Tools ${tools.length}
${barRankList(tools, { mapItem: x => ({ name: x.name, count: x.count }), maxOverride: maxTool, emptyText: 'No tool data', })}
`; renderChart(_usageSeries, usageChartMode); renderFrameworkBreakdown(s.by_framework); document.querySelectorAll('.usage-chart-tab').forEach(btn => { btn.addEventListener('click', () => { if (usageChartMode === btn.dataset.mode) return; usageChartMode = btn.dataset.mode; document.querySelectorAll('.usage-chart-tab').forEach(b => b.classList.toggle('active', b === btn)); renderChart(_usageSeries, usageChartMode); }); }); }