"use client"; import { useState, useEffect, useMemo, useCallback } from "react"; import { useSearchParams, useRouter } from "next/navigation"; import { FREE_PROVIDERS } from "@/shared/constants/providers"; import Badge from "./Badge"; import Card from "./Card"; import OverviewCards from "@/app/(dashboard)/dashboard/usage/components/OverviewCards"; import UsageTable, { fmt, fmtTime } from "@/app/(dashboard)/dashboard/usage/components/UsageTable"; import ProviderTopology from "@/app/(dashboard)/dashboard/usage/components/ProviderTopology"; import UsageChart from "@/app/(dashboard)/dashboard/usage/components/UsageChart"; function timeAgo(timestamp) { const diff = Math.floor((Date.now() - new Date(timestamp)) / 1000); if (diff < 60) return `${diff}s ago`; if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; return `${Math.floor(diff / 86400)}d ago`; } // Auto-update time display every second without re-rendering parent function TimeAgo({ timestamp }) { const [, setTick] = useState(0); useEffect(() => { const timer = setInterval(() => setTick(t => t + 1), 1000); return () => clearInterval(timer); }, []); return <>{timeAgo(timestamp)}; } function RecentRequests({ requests = [] }) { return ( {/* Header */}
Recent Requests
{!requests.length ? (
No requests yet.
) : (
{requests.map((r, i) => { const ok = !r.status || r.status === "ok" || r.status === "success"; return ( ); })}
Model In / Out When
{r.model} {fmt(r.promptTokens)}↑ {" "} {fmt(r.completionTokens)}↓
)}
); } function sortData(dataMap, pendingMap = {}, sortBy, sortOrder) { return Object.entries(dataMap || {}) .map(([key, data]) => { const totalTokens = (data.promptTokens || 0) + (data.completionTokens || 0); const totalCost = data.cost || 0; const inputCost = totalTokens > 0 ? (data.promptTokens || 0) * (totalCost / totalTokens) : 0; const outputCost = totalTokens > 0 ? (data.completionTokens || 0) * (totalCost / totalTokens) : 0; return { ...data, key, totalTokens, totalCost, inputCost, outputCost, pending: pendingMap[key] || 0 }; }) .sort((a, b) => { let valA = a[sortBy]; let valB = b[sortBy]; if (typeof valA === "string") valA = valA.toLowerCase(); if (typeof valB === "string") valB = valB.toLowerCase(); if (valA < valB) return sortOrder === "asc" ? -1 : 1; if (valA > valB) return sortOrder === "asc" ? 1 : -1; return 0; }); } function getGroupKey(item, keyField) { switch (keyField) { case "rawModel": return item.rawModel || "Unknown Model"; case "accountName": return item.accountName || `Account ${item.connectionId?.slice(0, 8)}...` || "Unknown Account"; case "keyName": return item.keyName || "Unknown Key"; case "endpoint": return item.endpoint || "Unknown Endpoint"; default: return item[keyField] || "Unknown"; } } function groupDataByKey(data, keyField) { if (!Array.isArray(data)) return []; const groups = {}; data.forEach((item) => { const gk = getGroupKey(item, keyField); if (!groups[gk]) { groups[gk] = { groupKey: gk, summary: { requests: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0, cost: 0, inputCost: 0, outputCost: 0, lastUsed: null, pending: 0 }, items: [], }; } const s = groups[gk].summary; s.requests += item.requests || 0; s.promptTokens += item.promptTokens || 0; s.completionTokens += item.completionTokens || 0; s.totalTokens += item.totalTokens || 0; s.cost += item.cost || 0; s.inputCost += item.inputCost || 0; s.outputCost += item.outputCost || 0; s.pending += item.pending || 0; if (item.lastUsed && (!s.lastUsed || new Date(item.lastUsed) > new Date(s.lastUsed))) { s.lastUsed = item.lastUsed; } groups[gk].items.push(item); }); return Object.values(groups); } const MODEL_COLUMNS = [ { field: "rawModel", label: "Model" }, { field: "provider", label: "Provider" }, { field: "requests", label: "Requests", align: "right" }, { field: "lastUsed", label: "Last Used", align: "right" }, ]; const ACCOUNT_COLUMNS = [ { field: "rawModel", label: "Model" }, { field: "provider", label: "Provider" }, { field: "accountName", label: "Account" }, { field: "requests", label: "Requests", align: "right" }, { field: "lastUsed", label: "Last Used", align: "right" }, ]; const API_KEY_COLUMNS = [ { field: "keyName", label: "API Key Name" }, { field: "rawModel", label: "Model" }, { field: "provider", label: "Provider" }, { field: "requests", label: "Requests", align: "right" }, { field: "lastUsed", label: "Last Used", align: "right" }, ]; const ENDPOINT_COLUMNS = [ { field: "endpoint", label: "Endpoint" }, { field: "rawModel", label: "Model" }, { field: "provider", label: "Provider" }, { field: "requests", label: "Requests", align: "right" }, { field: "lastUsed", label: "Last Used", align: "right" }, ]; const TABLE_OPTIONS = [ { value: "model", label: "Usage by Model" }, { value: "account", label: "Usage by Account" }, { value: "apiKey", label: "Usage by API Key" }, { value: "endpoint", label: "Usage by Endpoint" }, ]; const PERIODS = [ { value: "24h", label: "24h" }, { value: "7d", label: "7D" }, { value: "30d", label: "30D" }, { value: "60d", label: "60D" }, ]; export default function UsageStats() { const router = useRouter(); const searchParams = useSearchParams(); const sortBy = searchParams.get("sortBy") || "rawModel"; const sortOrder = searchParams.get("sortOrder") || "asc"; const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [fetching, setFetching] = useState(false); const [tableView, setTableView] = useState("model"); const [viewMode, setViewMode] = useState("costs"); const [providers, setProviders] = useState([]); const [period, setPeriod] = useState("7d"); // Fetch connected providers once, deduplicate by provider type // Always include noAuth free providers (e.g. opencode) regardless of connections useEffect(() => { fetch("/api/providers") .then((r) => r.ok ? r.json() : null) .then((d) => { const seen = new Set(); const unique = (d?.connections || []).filter((c) => { if (seen.has(c.provider)) return false; seen.add(c.provider); return true; }); const noAuthProviders = Object.values(FREE_PROVIDERS) .filter((p) => p.noAuth && !seen.has(p.id)) .map((p) => ({ provider: p.id, name: p.name })); setProviders([...unique, ...noAuthProviders]); }) .catch(() => {}); }, []); // Fetch filtered stats via REST when period changes useEffect(() => { // First load: show full spinner; subsequent: show subtle fetching indicator if (!stats) setLoading(true); else setFetching(true); fetch(`/api/usage/stats?period=${period}`) .then((r) => r.ok ? r.json() : null) .then((data) => { if (data) setStats((prev) => ({ ...prev, ...data })); }) .catch(() => {}) .finally(() => { setLoading(false); setFetching(false); }); }, [period]); // eslint-disable-line react-hooks/exhaustive-deps // SSE connection - real-time updates for activeRequests + recentRequests only useEffect(() => { const es = new EventSource("/api/usage/stream"); es.onmessage = (e) => { try { const data = JSON.parse(e.data); // Always merge only real-time fields, never overwrite full stats from REST setStats((prev) => ({ ...(prev || {}), activeRequests: data.activeRequests, recentRequests: data.recentRequests, errorProvider: data.errorProvider, pending: data.pending, })); setLoading(false); } catch (err) { console.error("[SSE CLIENT] parse error:", err); } }; es.onerror = () => setLoading(false); return () => es.close(); }, []); const toggleSort = useCallback((tableType, field) => { const params = new URLSearchParams(searchParams.toString()); if (params.get("sortBy") === field) { params.set("sortOrder", params.get("sortOrder") === "asc" ? "desc" : "asc"); } else { params.set("sortBy", field); params.set("sortOrder", "asc"); } router.replace(`?${params.toString()}`, { scroll: false }); }, [searchParams, router]); // Compute active table data const activeTableConfig = useMemo(() => { if (!stats) return null; switch (tableView) { case "model": { const pendingMap = stats.pending?.byModel || {}; return { columns: MODEL_COLUMNS, groupedData: groupDataByKey(sortData(stats.byModel, pendingMap, sortBy, sortOrder), "rawModel"), storageKey: "usage-stats:expanded-models", emptyMessage: "No usage recorded yet.", renderSummaryCells: (group) => ( <> — {fmt(group.summary.requests)} {fmtTime(group.summary.lastUsed)} ), renderDetailCells: (item) => ( <> 0 ? "text-primary" : ""}`}>{item.rawModel} 0 ? "primary" : "neutral"} size="sm">{item.provider} {fmt(item.requests)} {fmtTime(item.lastUsed)} ), }; } case "account": { const pendingMap = {}; if (stats?.pending?.byAccount) { Object.entries(stats.byAccount || {}).forEach(([accountKey, data]) => { const connPending = stats.pending.byAccount[data.connectionId]; if (connPending) { const modelKey = data.provider ? `${data.rawModel} (${data.provider})` : data.rawModel; pendingMap[accountKey] = connPending[modelKey] || 0; } }); } return { columns: ACCOUNT_COLUMNS, groupedData: groupDataByKey(sortData(stats.byAccount, pendingMap, sortBy, sortOrder), "accountName"), storageKey: "usage-stats:expanded-accounts", emptyMessage: "No account-specific usage recorded yet.", renderSummaryCells: (group) => ( <> — — {fmt(group.summary.requests)} {fmtTime(group.summary.lastUsed)} ), renderDetailCells: (item) => ( <> 0 ? "text-primary" : ""}`}>{item.accountName || `Account ${item.connectionId?.slice(0, 8)}...`} 0 ? "text-primary" : ""}`}>{item.rawModel} 0 ? "primary" : "neutral"} size="sm">{item.provider} {fmt(item.requests)} {fmtTime(item.lastUsed)} ), }; } case "apiKey": { return { columns: API_KEY_COLUMNS, groupedData: groupDataByKey(sortData(stats.byApiKey, {}, sortBy, sortOrder), "keyName"), storageKey: "usage-stats:expanded-apikeys", emptyMessage: "No API key usage recorded yet.", renderSummaryCells: (group) => ( <> — — {fmt(group.summary.requests)} {fmtTime(group.summary.lastUsed)} ), renderDetailCells: (item) => ( <> {item.keyName} {item.rawModel} {item.provider} {fmt(item.requests)} {fmtTime(item.lastUsed)} ), }; } case "endpoint": default: { return { columns: ENDPOINT_COLUMNS, groupedData: groupDataByKey(sortData(stats.byEndpoint, {}, sortBy, sortOrder), "endpoint"), storageKey: "usage-stats:expanded-endpoints", emptyMessage: "No endpoint usage recorded yet.", renderSummaryCells: (group) => ( <> — — {fmt(group.summary.requests)} {fmtTime(group.summary.lastUsed)} ), renderDetailCells: (item) => ( <> {item.endpoint} {item.rawModel} {item.provider} {fmt(item.requests)} {fmtTime(item.lastUsed)} ), }; } } }, [stats, tableView, sortBy, sortOrder]); if (!stats && !loading) return
Failed to load usage statistics.
; const spinner = (
progress_activity
); return (
{/* Period selector */}
{PERIODS.map((p) => ( ))}
{fetching && ( progress_activity )}
{/* Overview cards */} {loading ? spinner : } {/* Provider topology + Recent Requests */} {loading ? spinner : (
)} {/* Token / Cost chart - sync period */} {loading ? spinner : } {/* Table with dropdown selector */}
{loading ? spinner : activeTableConfig && ( )}
); }