SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
20.7 KB · 397 lines tsx
Raw Blame History
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