TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1"use client";2import * as React from "react";3import { RefreshCw, ChevronDown, CheckCircle2, XCircle, Users, Boxes, Link2, AlertTriangle } from "lucide-react";4import { PageHeader, Card, CardHeader, CardBody, Stat, EmptyState, Skeleton } from "@/components/ui/misc";5import { Button } from "@/components/ui/button";6import { Badge } from "@/components/ui/badge";7import { Tooltip } from "@/components/ui/tooltip";8import { toast } from "@/components/ui/toast";9import { ProviderIcon } from "@/components/brand/provider-icon";10import { api, useApi } from "@/lib/client/api";11import { PROVIDERS, PROVIDER_ORDER, providerName } from "@/lib/client/providers";12import { formatMs, formatNumber, formatRelative, cn } from "@/lib/utils";13import { errorMessage, formatDateTime } from "@/lib/client/humanize";14import type { ProviderId } from "@/lib/client/types";1516interface AdminResponse {17 users: number;18 connections: { provider: ProviderId; status: string; n: number }[];19 counts: { provider: ProviderId; total: number; hidden: number }[];20 syncRuns: {21 id: string;22 provider: ProviderId;23 triggeredBy: string;24 startedAt: string;25 finishedAt: string | null;26 ok: boolean | null;27 modelsFound: number | null;28 modelsAdded: number | null;29 modelsRemoved: number | null;30 latencyMs: number | null;31 errorCode: string | null;32 errorMessage: string | null;33 }[];34 errors: { provider: ProviderId; errorCode: string | null; n: number }[];35 latency: { provider: ProviderId; avgLatencyMs: number; avgTtftMs: number; requests: number }[];36 providers: { provider: ProviderId; envKeyConfigured: boolean; models: { key: string; displayName: string; status: string; hidden: boolean; lastSeenAt: string; source: string }[] }[];37}3839interface SyncResult {40 provider: ProviderId;41 ok: boolean;42 found: number;43 added: number;44 removed: number;45 latencyMs: number;46 error?: string;47}4849const CONN_STATUS_VARIANT: Record<string, "success" | "danger" | "warning" | "default"> = { valid: "success", invalid: "danger", unverified: "warning" };5051export default function AdminProvidersPage() {52 const q = useApi<AdminResponse>("/api/admin/providers");53 const [syncing, setSyncing] = React.useState<string | null>(null);54 const data = q.data;5556 const sync = async (provider?: ProviderId) => {57 setSyncing(provider ?? "all");58 try {59 const res = await api<{ results: SyncResult[] }>("/api/admin/providers", { method: "POST", json: provider ? { action: "sync", provider } : { action: "sync" } });60 await q.mutate();61 const ok = res.results.filter((r) => r.ok);62 const bad = res.results.filter((r) => !r.ok);63 if (ok.length) toast.success(`Synced ${ok.map((r) => providerName(r.provider)).join(", ")}`, ok.map((r) => `${providerName(r.provider)}: ${r.found} found, +${r.added} / −${r.removed} in ${formatMs(r.latencyMs)}`).join(" · "));64 for (const r of bad) toast.error(`${providerName(r.provider)} failed`, r.error);65 if (!res.results.length) toast.warning("Nothing synced", "No owner keys configured in the environment.");66 } catch (e) {67 toast.error("Sync failed", errorMessage(e));68 } finally {69 setSyncing(null);70 }71 };7273 const connByProvider = React.useMemo(() => {74 const m = new Map<ProviderId, Record<string, number>>();75 for (const c of data?.connections ?? []) {76 const cur = m.get(c.provider) ?? {};77 cur[c.status] = (cur[c.status] ?? 0) + c.n;78 m.set(c.provider, cur);79 }80 return m;81 }, [data?.connections]);8283 const totalConnections = (data?.connections ?? []).reduce((a, c) => a + c.n, 0);84 const totalModels = (data?.counts ?? []).reduce((a, c) => a + c.total, 0);85 const totalErrors = (data?.errors ?? []).reduce((a, c) => a + c.n, 0);8687 return (88 <div className="space-y-6">89 <PageHeader90 title="Provider diagnostics"91 description="Registry health, sync history and 7-day error/latency signals across all users. User key material is never loaded here."92 actions={93 <Button loading={syncing === "all"} disabled={syncing !== null} onClick={() => sync()}>94 <RefreshCw /> Sync all with owner keys95 </Button>96 }97 />9899 {q.error ? (100 <EmptyState title="Could not load diagnostics" description={errorMessage(q.error)} action={<Button variant="outline" size="sm" onClick={() => q.mutate()}>Retry</Button>} />101 ) : !data ? (102 <div className="space-y-4">103 <div className="grid grid-cols-2 gap-3 md:grid-cols-4">104 {Array.from({ length: 4 }).map((_, i) => (105 <Skeleton key={i} className="h-[76px]" />106 ))}107 </div>108 <Skeleton className="h-64" />109 </div>110 ) : (111 <>112 <div className="grid grid-cols-2 gap-3 md:grid-cols-4">113 <Stat label="Users" value={formatNumber(data.users)} hint={<span className="inline-flex items-center gap-1"><Users className="size-3" /> registered</span>} />114 <Stat label="Provider connections" value={formatNumber(totalConnections)} hint={<span className="inline-flex items-center gap-1"><Link2 className="size-3" /> across all users</span>} />115 <Stat label="Models in registry" value={formatNumber(totalModels)} hint={<span className="inline-flex items-center gap-1"><Boxes className="size-3" /> {formatNumber((data.counts ?? []).reduce((a, c) => a + c.hidden, 0))} hidden</span>} />116 <Stat label="Errors (7 d)" value={formatNumber(totalErrors)} hint={totalErrors ? "see breakdown below" : "no failures"} className={totalErrors ? "border-warning/40" : undefined} />117 </div>118119 {/* Per-provider overview */}120 <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">121 {PROVIDER_ORDER.map((p) => {122 const conns = connByProvider.get(p) ?? {};123 const count = data.counts.find((c) => c.provider === p);124 const lat = data.latency.find((l) => l.provider === p);125 const env = data.providers.find((x) => x.provider === p)?.envKeyConfigured ?? false;126 const lastRun = data.syncRuns.find((r) => r.provider === p);127 return (128 <Card key={p} className="p-4">129 <div className="flex items-start justify-between gap-2">130 <div className="flex items-center gap-2">131 <ProviderIcon provider={p} size={18} />132 <h3 className="text-sm font-semibold">{PROVIDERS[p].name}</h3>133 </div>134 <Tooltip content={env ? "Owner key present in environment — admin sync available" : "No owner key in environment; registry relies on user-triggered syncs"}>135 <Badge variant={env ? "success" : "default"}>{env ? "owner key" : "no owner key"}</Badge>136 </Tooltip>137 </div>138 <dl className="mt-3 space-y-1.5 text-xs">139 <div className="flex justify-between gap-2">140 <dt className="text-fg-subtle">Connections</dt>141 <dd className="flex flex-wrap justify-end gap-1">142 {Object.keys(conns).length ? (143 Object.entries(conns).map(([s, n]) => (144 <Badge key={s} variant={CONN_STATUS_VARIANT[s] ?? "default"}>145 {n} {s}146 </Badge>147 ))148 ) : (149 <span className="text-fg-muted">none</span>150 )}151 </dd>152 </div>153 <div className="flex justify-between gap-2">154 <dt className="text-fg-subtle">Registry</dt>155 <dd className="font-mono tabular-nums">156 {count?.total ?? 0} <span className="text-fg-subtle">({count?.hidden ?? 0} hidden)</span>157 </dd>158 </div>159 <div className="flex justify-between gap-2">160 <dt className="text-fg-subtle">Latency (7 d)</dt>161 <dd className="font-mono tabular-nums">{lat && lat.requests ? `${formatMs(lat.avgLatencyMs)} · ttft ${formatMs(lat.avgTtftMs)} · ${formatNumber(lat.requests)} req` : "—"}</dd>162 </div>163 <div className="flex justify-between gap-2">164 <dt className="text-fg-subtle">Last sync</dt>165 <dd className="flex items-center gap-1">166 {lastRun ? (167 <>168 {lastRun.ok ? <CheckCircle2 className="size-3 text-success" /> : lastRun.ok === false ? <XCircle className="size-3 text-danger" /> : null}169 <span className="text-fg-muted">{formatRelative(lastRun.startedAt)}</span>170 </>171 ) : (172 <span className="text-fg-muted">never</span>173 )}174 </dd>175 </div>176 </dl>177 <Button size="sm" variant="outline" className="mt-3 w-full" disabled={!env || syncing !== null} loading={syncing === p} onClick={() => sync(p)}>178 <RefreshCw /> Sync {PROVIDERS[p].shortName}179 </Button>180 </Card>181 );182 })}183 </div>184185 {/* Errors + latency */}186 <div className="grid gap-3 lg:grid-cols-2">187 <Card>188 <CardHeader title="Errors by provider and code" description="Last 7 days, top 30." />189 <CardBody className="px-0 pb-0">190 {data.errors.length === 0 ? (191 <p className="px-5 pb-5 text-sm text-fg-muted">No failed requests in the last 7 days.</p>192 ) : (193 <table className="w-full text-[13px]">194 <thead>195 <tr className="border-y border-border bg-bg-subtle/60 text-left text-[11px] font-medium uppercase tracking-wide text-fg-subtle">196 <th className="px-5 py-2">Provider</th>197 <th className="px-3 py-2">Code</th>198 <th className="px-5 py-2 text-right">Count</th>199 </tr>200 </thead>201 <tbody>202 {data.errors.map((e, i) => (203 <tr key={`${e.provider}-${e.errorCode}-${i}`} className="border-b border-border last:border-b-0">204 <td className="px-5 py-2">205 <span className="flex items-center gap-1.5">206 <ProviderIcon provider={e.provider} size={13} /> {providerName(e.provider)}207 </span>208 </td>209 <td className="px-3 py-2">210 <code className="rounded bg-danger-soft px-1.5 py-0.5 text-[11px] text-danger">{e.errorCode ?? "unknown"}</code>211 </td>212 <td className="px-5 py-2 text-right font-mono tabular-nums">{formatNumber(e.n)}</td>213 </tr>214 ))}215 </tbody>216 </table>217 )}218 </CardBody>219 </Card>220 <Card>221 <CardHeader title="Latency by provider" description="Average total and time-to-first-token, last 7 days." />222 <CardBody className="space-y-3">223 {data.latency.length === 0 ? (224 <p className="text-sm text-fg-muted">No requests in the last 7 days.</p>225 ) : (226 (() => {227 const max = Math.max(...data.latency.map((l) => l.avgLatencyMs), 1);228 return [...data.latency]229 .sort((a, b) => a.avgLatencyMs - b.avgLatencyMs)230 .map((l) => (231 <div key={l.provider} className="space-y-1">232 <div className="flex items-center justify-between text-xs">233 <span className="flex items-center gap-1.5 font-medium">234 <ProviderIcon provider={l.provider} size={13} /> {providerName(l.provider)}235 <span className="text-fg-subtle">· {formatNumber(l.requests)} req</span>236 </span>237 <span className="font-mono tabular-nums text-fg-muted">238 {formatMs(l.avgLatencyMs)} <span className="text-fg-subtle">/ ttft {formatMs(l.avgTtftMs)}</span>239 </span>240 </div>241 <div className="relative h-2 overflow-hidden rounded-full bg-bg-muted">242 <div className="absolute inset-y-0 left-0 rounded-full" style={{ width: `${(l.avgLatencyMs / max) * 100}%`, background: PROVIDERS[l.provider]?.colorVar ?? "var(--fg-subtle)" }} />243 <div className="absolute inset-y-0 left-0 rounded-full bg-bg-elevated/60" style={{ width: `${(l.avgTtftMs / max) * 100}%` }} aria-hidden />244 </div>245 </div>246 ));247 })()248 )}249 </CardBody>250 </Card>251 </div>252253 {/* Sync runs */}254 <Card>255 <CardHeader title="Model sync runs" description="Most recent 40 registry refreshes (any trigger)." />256 <CardBody className="px-0 pb-0">257 {data.syncRuns.length === 0 ? (258 <p className="px-5 pb-5 text-sm text-fg-muted">No sync runs recorded yet.</p>259 ) : (260 <div className="overflow-x-auto">261 <table className="w-full min-w-[760px] text-[13px]">262 <thead>263 <tr className="border-y border-border bg-bg-subtle/60 text-left text-[11px] font-medium uppercase tracking-wide text-fg-subtle">264 <th className="px-5 py-2">Provider</th>265 <th className="px-3 py-2">Trigger</th>266 <th className="px-3 py-2">Started</th>267 <th className="px-3 py-2">Result</th>268 <th className="px-3 py-2 text-right">Found</th>269 <th className="px-3 py-2 text-right">Added</th>270 <th className="px-3 py-2 text-right">Removed</th>271 <th className="px-3 py-2 text-right">Latency</th>272 <th className="px-5 py-2">Error</th>273 </tr>274 </thead>275 <tbody>276 {data.syncRuns.map((r) => (277 <tr key={r.id} className="border-b border-border last:border-b-0">278 <td className="px-5 py-2">279 <span className="flex items-center gap-1.5">280 <ProviderIcon provider={r.provider} size={13} /> {providerName(r.provider)}281 </span>282 </td>283 <td className="px-3 py-2">284 <Badge className="capitalize">{r.triggeredBy}</Badge>285 </td>286 <td className="px-3 py-2 text-fg-muted">287 <Tooltip content={formatDateTime(r.startedAt, { year: "numeric", second: "2-digit" })}>288 <span>{formatRelative(r.startedAt)}</span>289 </Tooltip>290 </td>291 <td className="px-3 py-2">292 {r.ok === true ? (293 <Badge variant="success">294 <CheckCircle2 /> ok295 </Badge>296 ) : r.ok === false ? (297 <Badge variant="danger">298 <XCircle /> failed299 </Badge>300 ) : (301 <Badge variant="warning">running</Badge>302 )}303 </td>304 <td className="px-3 py-2 text-right font-mono tabular-nums">{r.modelsFound ?? "—"}</td>305 <td className="px-3 py-2 text-right font-mono tabular-nums text-success">{r.modelsAdded ? `+${r.modelsAdded}` : "0"}</td>306 <td className="px-3 py-2 text-right font-mono tabular-nums text-warning">{r.modelsRemoved ? `−${r.modelsRemoved}` : "0"}</td>307 <td className="px-3 py-2 text-right font-mono tabular-nums text-fg-muted">{formatMs(r.latencyMs)}</td>308 <td className="max-w-[240px] truncate px-5 py-2 text-xs text-fg-muted" title={r.errorMessage ?? undefined}>309 {r.errorCode ? <code className="mr-1 rounded bg-danger-soft px-1 text-[11px] text-danger">{r.errorCode}</code> : null}310 {r.errorMessage ?? ""}311 </td>312 </tr>313 ))}314 </tbody>315 </table>316 </div>317 )}318 </CardBody>319 </Card>320321 {/* Per-provider model lists */}322 <section className="space-y-2" aria-label="Registry models per provider">323 <h2 className="text-[15px] font-semibold tracking-tight">Registry contents</h2>324 {data.providers.map((p) => (325 <ProviderModelList key={p.provider} provider={p.provider} models={p.models} />326 ))}327 </section>328 </>329 )}330 </div>331 );332}333334function ProviderModelList({ provider, models }: { provider: ProviderId; models: AdminResponse["providers"][number]["models"] }) {335 const [open, setOpen] = React.useState(false);336 const [now] = React.useState(() => Date.now());337 const hidden = models.filter((m) => m.hidden).length;338 const stale = models.filter((m) => now - new Date(m.lastSeenAt).getTime() > 7 * 86_400_000 && !m.hidden).length;339 return (340 <Card>341 <button type="button" onClick={() => setOpen((v) => !v)} aria-expanded={open} className="flex w-full items-center justify-between gap-3 px-4 py-3 text-left">342 <span className="flex items-center gap-2.5">343 <ProviderIcon provider={provider} size={16} />344 <span className="text-sm font-medium">{PROVIDERS[provider].name}</span>345 <span className="text-xs text-fg-subtle">346 {models.length} models · {hidden} hidden347 </span>348 {stale ? (349 <Tooltip content="Visible models not seen on the provider list in over 7 days">350 <Badge variant="warning">351 <AlertTriangle /> {stale} stale352 </Badge>353 </Tooltip>354 ) : null}355 </span>356 <ChevronDown className={cn("size-4 text-fg-subtle transition-transform", open && "rotate-180")} />357 </button>358 {open ? (359 <div className="overflow-x-auto border-t border-border">360 {models.length === 0 ? (361 <p className="px-4 py-3 text-sm text-fg-muted">No models recorded for this provider.</p>362 ) : (363 <table className="w-full min-w-[640px] text-[13px]">364 <thead>365 <tr className="bg-bg-subtle/60 text-left text-[11px] font-medium uppercase tracking-wide text-fg-subtle">366 <th className="px-4 py-2">Key</th>367 <th className="px-3 py-2">Name</th>368 <th className="px-3 py-2">Status</th>369 <th className="px-3 py-2">Source</th>370 <th className="px-3 py-2">Hidden</th>371 <th className="px-4 py-2 text-right">Last seen</th>372 </tr>373 </thead>374 <tbody>375 {models.map((m) => (376 <tr key={m.key} className={cn("border-t border-border", m.hidden && "opacity-60")}>377 <td className="px-4 py-1.5 font-mono text-xs">{m.key}</td>378 <td className="px-3 py-1.5">{m.displayName}</td>379 <td className="px-3 py-1.5">380 <Badge variant={m.status === "active" ? "success" : m.status === "preview" ? "info" : m.status === "deprecated" ? "warning" : "default"} className="capitalize">381 {m.status}382 </Badge>383 </td>384 <td className="px-3 py-1.5 text-xs text-fg-muted">{m.source}</td>385 <td className="px-3 py-1.5 text-xs text-fg-muted">{m.hidden ? "yes" : "no"}</td>386 <td className="px-4 py-1.5 text-right text-xs text-fg-muted">{formatRelative(m.lastSeenAt)}</td>387 </tr>388 ))}389 </tbody>390 </table>391 )}392 </div>393 ) : null}394 </Card>395 );396}397