"use client"; import { useMemo, useState } from "react"; import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { Modal, ModalTitle, ModalDescription, ModalClose, } from "@/components/modal"; import type { AdminErrorOverview, AdminErrorRow, AdminErrorsResponse, } from "@ttsa/shared"; import type { ColumnDef, PaginationState } from "@tanstack/react-table"; import { PageHeader, StatCard } from "@/components/admin/shell"; import { DataTable, fmtDate, truncate } from "@/components/admin/data-table"; import { BarChartCard, HBarChartCard } from "@/components/admin/charts"; /* A small fixed palette so each source gets a stable color in the stacked chart. */ const PALETTE = [ "var(--color-accent)", "#f59e0b", "#3b82f6", "#10b981", "#a855f7", "#ef4444", "#14b8a6", "#eab308", "#ec4899", "#6366f1", ]; const SEV_COLOR: Record = { fatal: "text-accent", error: "text-accent", warn: "text-amber-500", }; async function fetchOverview(): Promise { const res = await fetch("/api/admin/errors/overview"); if (!res.ok) throw new Error("failed"); return res.json(); } async function fetchErrors(params: { page: number; pageSize: number; source?: string; severity?: string; search?: string; }): Promise { const sp = new URLSearchParams({ page: String(params.page), pageSize: String(params.pageSize), }); if (params.source) sp.set("source", params.source); if (params.severity) sp.set("severity", params.severity); if (params.search) sp.set("search", params.search); const res = await fetch(`/api/admin/errors?${sp}`); if (!res.ok) throw new Error("failed"); return res.json(); } export default function AdminErrorsPage() { const { data: ov } = useQuery({ queryKey: ["admin", "errors", "overview"], queryFn: fetchOverview, refetchInterval: 30_000, }); return (
{!ov ? (

Loading…

) : (
{/* Errors per day, stacked by source */} ({ date: d.date, ...d.bySource }))} xKey="date" stacked height={240} series={ov.sources.map((s, i) => ({ key: s, label: s, color: PALETTE[i % PALETTE.length], }))} />
{ov.byModel.length > 0 ? ( ({ model: truncate(m.model, 18), count: m.count, }))} labelKey="model" valueKey="count" /> ) : ( )} {ov.byProvider.length > 0 ? ( ({ provider: truncate(p.provider, 18), count: p.count, }))} labelKey="provider" valueKey="count" color="#3b82f6" /> ) : ( )}
{/* Breakdown by source */}

Errors by source (7d)

    {ov.bySource.length === 0 && (
  • No errors. Nice.
  • )} {ov.bySource.map((s) => (
  • {s.source} {s.count}
  • ))}
)}
); } function EmptyCard({ title }: { title: string }) { return (

{title}

No data in the window.

); } function ErrorLog({ sources }: { sources: string[] }) { const [source, setSource] = useState(""); const [severity, setSeverity] = useState(""); const [search, setSearch] = useState(""); const [selected, setSelected] = useState(null); const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 50, }); const { data, isLoading, isFetching } = useQuery({ queryKey: [ "admin", "errors", "list", pagination.pageIndex, pagination.pageSize, source, severity, search, ], queryFn: () => fetchErrors({ page: pagination.pageIndex, pageSize: pagination.pageSize, source: source || undefined, severity: severity || undefined, search: search || undefined, }), placeholderData: keepPreviousData, }); const pageCount = data ? Math.max(1, Math.ceil(data.total / pagination.pageSize)) : 1; const resetPage = () => setPagination((p) => ({ ...p, pageIndex: 0 })); const columns = useMemo[]>( () => [ { accessorKey: "createdAt", header: "When", enableSorting: false, cell: (c) => ( {fmtDate(c.row.original.createdAt)} ), }, { accessorKey: "severity", header: "Sev", enableSorting: false, cell: (c) => ( {c.row.original.severity} ), }, { accessorKey: "source", header: "Source", enableSorting: false, cell: (c) => ( {c.row.original.source} ), }, { id: "target", header: "Model / Provider", enableSorting: false, cell: (c) => { const r = c.row.original; if (!r.model && !r.provider) return ; return ( {r.model ?? r.provider} {r.status ? ( ({r.status}) ) : null} ); }, }, { accessorKey: "message", header: "Message", enableSorting: false, cell: (c) => ( {truncate(c.row.original.message, 70)} ), }, ], [], ); return (
{ setSearch(e.target.value); resetPage(); }} placeholder="Search message…" className="w-56 rounded-md border border-line bg-transparent px-3 py-1.5 text-sm outline-none focus:border-ink-3" /> {data && ( {data.total.toLocaleString()} events )}
setSelected(null)} />
); } function ErrorDetailDialog({ row, onClose, }: { row: AdminErrorRow | null; onClose: () => void; }) { return ( {row && (
{row.source} {row.route ? ` · ${row.method ?? ""} ${row.route}` : ""} {row.message}

{fmtDate(row.createdAt)}

{row.detail && (

Detail

                {prettyJson(row.detail)}
              
)} {row.stack && (

Stack

                {row.stack}
              
)}
Close
)}
); } function Meta({ label, value }: { label: string; value: string | null }) { return (

{label}

{value || "—"}

); } function prettyJson(s: string): string { try { return JSON.stringify(JSON.parse(s), null, 2); } catch { return s; } }