import { useEffect, useState } from "react" import { Activity, AlertTriangle, CheckCircle2, ChevronDown, ChevronLeft, ChevronRight, RefreshCw, XCircle, } from "lucide-react" import { errorMessage } from "@/App" import { fetchUsageStats } from "@/lib/api" import type { UsageRecord, UsageResult } from "@/lib/types" import { RANGES, buildBuckets, computeKpis, filterByRange, formatDuration, formatShortTime, formatTimestamp, type RangeKey, } from "@/lib/usage-data" import { cn } from "@/lib/utils" import { HourHeatmap } from "./usage/HourHeatmap" import { KpiCards } from "./usage/KpiCards" import { RoutingSankey } from "./usage/RoutingSankey" import { TokenDonut } from "./usage/TokenDonut" import { TopEndpoints } from "./usage/TopEndpoints" import { VolumeAreaChart } from "./usage/VolumeAreaChart" import { Badge } from "./ui/badge" import { Button } from "./ui/button" import { Card, CardContent } from "./ui/card" import { Skeleton } from "./ui/skeleton" const REFRESH_INTERVAL_MS = 10_000 const PAGE_SIZES = [10, 25, 50] as const /** Compact page-number list with ellipses for large logs (1 … 3 4 5 … 40). */ function pageList(current: number, total: number): (number | "…")[] { if (total <= 7) return Array.from({ length: total }, (_, index) => index + 1) const pages: (number | "…")[] = [1] if (current > 3) pages.push("…") const start = Math.max(2, current - 1) const end = Math.min(total - 1, current + 1) for (let pageNumber = start; pageNumber <= end; pageNumber += 1) { pages.push(pageNumber) } if (current < total - 2) pages.push("…") pages.push(total) return pages } const STATUS_VARIANT = { success: "ok", error: "error", cancelled: "neutral", } as const const STATUS_ICON = { success: , error: , cancelled: , } as const function statusLabel(status: UsageRecord["status"]): string { return status === "cancelled" ? "Cancelled" : status === "error" ? "Error" : "Success" } function RangeSelector({ value, onChange, }: { value: RangeKey onChange: (range: RangeKey) => void }) { return (
{RANGES.map((option) => ( ))}
) } export function UsageView() { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [expandedId, setExpandedId] = useState(null) const [range, setRange] = useState("1h") const [updatedAt, setUpdatedAt] = useState(null) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(10) const refresh = async () => { try { const result = await fetchUsageStats() setData(result) setError(null) setUpdatedAt(Date.now() / 1000) } catch (err) { setError(errorMessage(err)) } finally { setLoading(false) } } useEffect(() => { void refresh() const timer = setInterval(() => { void refresh() }, REFRESH_INTERVAL_MS) return () => clearInterval(timer) // The interval deliberately captures the initial refresh closure. // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const records = data?.records ?? [] const now = Date.now() / 1000 const filtered = filterByRange(records, range, now) const buckets = buildBuckets(filtered, now) const kpis = computeKpis(filtered, now) // The request log is paginated; the page clamps when a refresh shrinks it. const totalPages = Math.max(1, Math.ceil(records.length / pageSize)) const currentPage = Math.min(page, totalPages) const pageStart = (currentPage - 1) * pageSize const pageRecords = records.slice(pageStart, pageStart + pageSize) const showingStart = records.length === 0 ? 0 : pageStart + 1 const showingEnd = Math.min(records.length, pageStart + pageSize) if (loading && !data) { return (
{Array.from({ length: 6 }).map((_, index) => ( ))}
) } if (error && !data) { return (

Could not load usage statistics: {error}

) } const statusSummary = { success: records.reduce( (sum, record) => sum + (record.status === "success" ? 1 : 0), 0, ), error: records.reduce( (sum, record) => sum + (record.status === "error" ? 1 : 0), 0, ), cancelled: records.reduce( (sum, record) => sum + (record.status === "cancelled" ? 1 : 0), 0, ), } return (

Usage

Token throughput and request routing for the gateway, refreshed automatically every 10 seconds. Full prompts are captured for each request (capped at 64 KB) in ~/.fcc/usage.json.

{updatedAt !== null ? (

Updated {formatShortTime(updatedAt)}

) : null}
{records.length === 0 ? (

No requests tracked yet. Send a request through the proxy and it will appear here.

) : filtered.length === 0 ? (

No requests in the selected window. Try a wider range.

) : ( <>
)}

Recent requests

{records.length} tracked · newest first

{STATUS_ICON.success} {statusSummary.success} {STATUS_ICON.error} {statusSummary.error} {STATUS_ICON.cancelled} {statusSummary.cancelled}
{pageRecords.map((record) => { const expanded = record.request_id === expandedId const cacheTokens = record.cache_creation_tokens + record.cache_read_tokens return ( setExpandedId(expanded ? null : record.request_id) } /> ) })}
Time Provider / Model Input Output Cache Reasoning Duration Status
{records.length > pageSize ? (

Showing{" "} {showingStart}–{showingEnd} {" "} of {records.length}

Per page
{PAGE_SIZES.map((size) => ( ))}
) : null}
) } interface UsageRowProps { record: UsageRecord expanded: boolean cacheTokens: number onToggle: () => void } function UsageRow({ record, expanded, cacheTokens, onToggle }: UsageRowProps) { const modelLabel = record.gateway_model || record.provider_model const cacheRead = record.cache_read_tokens const cacheCreation = record.cache_creation_tokens return ( <> {expanded ? ( ) : ( )} {formatTimestamp(record.timestamp)}

{modelLabel}

{record.provider} {record.provider_model && record.provider_model !== modelLabel ? ` → ${record.provider_model}` : ""}

{record.input_tokens} {record.output_tokens} {cacheTokens > 0 ? ( {cacheTokens} ) : ( )} {record.reasoning_tokens > 0 ? ( record.reasoning_tokens ) : ( )} {formatDuration(record.duration_ms)} {STATUS_ICON[record.status]} {statusLabel(record.status)} {expanded ? (
{record.wire_api} {record.error_type ? ( {record.error_type} ) : null} {record.request_id}
                {record.prompt || "(no prompt captured)"}
              
) : null} ) }