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). */}