import { useState } from 'react' import { useQuery } from '@tanstack/react-query' import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LineChart, Line, Legend, } from 'recharts' import { apiFetch } from '@/lib/api' import { Button } from '@/components/ui/button' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { PageHeader } from '@/components/page-header' import { Tooltip as HoverTooltip } from '@/components/tooltip' import { formatSqliteUtcToLocalTime } from '@/lib/utils' type TimeRange = '24h' | '7d' | '30d' function formatTokens(n?: number): string { if (!n) return '0' if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M` if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K` return String(n) } function Stat({ label, value, hint, className }: { label: string; value: string | number; hint?: string; className?: string }) { const card = (

{label}

{value}

) // Same portal tooltip as the routing strategy chips. Opens BELOW the card: // the stats row sits right under the sticky navbar. return hint ? {card} : card } function Panel({ title, children }: { title: string; children: React.ReactNode }) { return (

{title}

{children}
) } const axisStyle = { fontSize: 11, fill: 'var(--muted-foreground)' } as const const gridStyle = 'var(--border)' const primaryFill = 'var(--foreground)' export default function AnalyticsPage() { const [range, setRange] = useState('7d') const { data: summary } = useQuery({ queryKey: ['analytics', 'summary', range], queryFn: () => apiFetch(`/api/analytics/summary?range=${range}`), }) const { data: byPlatform = [] } = useQuery({ queryKey: ['analytics', 'by-platform', range], queryFn: () => apiFetch(`/api/analytics/by-platform?range=${range}`), }) const { data: timeline = [] } = useQuery({ queryKey: ['analytics', 'timeline', range], queryFn: () => apiFetch(`/api/analytics/timeline?range=${range}`), }) const { data: byModel = [] } = useQuery({ queryKey: ['analytics', 'by-model', range], queryFn: () => apiFetch(`/api/analytics/by-model?range=${range}`), }) const { data: errors = [] } = useQuery({ queryKey: ['analytics', 'errors', range], queryFn: () => apiFetch(`/api/analytics/errors?range=${range}`), }) const { data: errorDist } = useQuery({ queryKey: ['analytics', 'error-distribution', range], queryFn: () => apiFetch<{ byCategory: any[]; byPlatform: any[]; detailed: any[] }>(`/api/analytics/error-distribution?range=${range}`), }) // Savings card shows ONE stable monthly figure regardless of the selected // range: the last-30-days data projected to a full month from its actual // span (a young install with 2 days of data shows 15x its 2-day total). // Once 30 days of history exist the real total shows as-is. The hover // hint carries the selected period's actual amount and the projection // basis. Querying 30d separately is free: react-query shares the cache // with the 30d tab. const { data: summary30 } = useQuery({ queryKey: ['analytics', 'summary', '30d'], queryFn: () => apiFetch(`/api/analytics/summary?range=30d`), }) const actualSavings = summary?.estimatedCostSavings ?? 0 const baseSavings = summary30?.estimatedCostSavings ?? 0 const spanDays = (() => { if (!summary30?.firstRequestAt) return 30 // SQLite stores UTC "YYYY-MM-DD HH:MM:SS" const first = new Date(summary30.firstRequestAt.replace(' ', 'T') + 'Z').getTime() const days = (Date.now() - first) / 86_400_000 if (!Number.isFinite(days)) return 30 return Math.min(Math.max(days, 1 / 24), 30) })() const extrapolated = spanDays < 29.5 const savings30d = extrapolated ? baseSavings * (30 / spanDays) : baseSavings const rangeLabel = range === '24h' ? '24 hours' : range === '7d' ? '7 days' : '30 days' const spanLabel = spanDays >= 2 ? `${Math.round(spanDays)} days` : `${Math.max(1, Math.round(spanDays * 24))} hours` const savingsHint = `You actually saved $${actualSavings.toFixed(2)} over the last ${rangeLabel}. That is what the same tokens would have cost on paid APIs, priced per model. ` + (extrapolated ? `The number shown projects your pace from the last ${spanLabel} of data to a full 30 days.` : `The number shown is your real 30-day total.`) // Pinned = the client named a specific model instead of auto-routing. // Honored = that model actually served it (the rest failed over). const pinned = summary?.pinnedRequests ?? 0 const pinHonored = summary?.pinHonoredRequests ?? 0 const requestsHint = pinned > 0 ? `${pinned} of these requests pinned a specific model by name. ${pinHonored} were served by the pinned model; ${pinned - pinHonored} failed over to a different one. The rest were auto-routed.` : 'All requests in this period were auto-routed; no client pinned a specific model by name.' return (
{(['24h', '7d', '30d'] as TimeRange[]).map(r => ( ))}
} />
{/* Summary stats */}
{/* Priced per request at the served model's paid-API equivalent rate (not a flat frontier-model rate) — see db/model-pricing.ts. The value is a 30-day projection; the hover hint tells the whole story (actual period amount + whether it was extrapolated). */}
{byPlatform.length === 0 ? (

No data yet

) : ( )}
{byPlatform.length === 0 ? (

No data yet

) : ( )}
{timeline.length === 0 ? (

No data yet

) : ( )}
{byModel.length === 0 ? (

No data yet

) : (
Model Provider Requests Pinned Success Latency In tokens Out tokens Saved {byModel.map((m: any, i: number) => ( {m.displayName} {m.platform} {m.requests} {m.pinnedRequests > 0 ? m.pinnedRequests : '—'} {m.successRate}% {m.avgLatencyMs} ms {formatTokens(m.totalInputTokens)} {formatTokens(m.totalOutputTokens)} ${(m.estimatedCost ?? 0).toFixed(2)} ))}
)}
{!errorDist?.byPlatform?.length ? (

No errors

) : ( )}
{errors.length === 0 ? (

No errors

) : (
Provider Message Time {errors.slice(0, 20).map((e: any) => ( {e.platform} {e.error} {formatSqliteUtcToLocalTime(e.createdAt, { hour: '2-digit', minute: '2-digit' })} ))}
)}
) }