/** * Server-side error view for the cost center: **a single panel** — a row of small * stats up top (total / unexpected / expected / most common error code), * with a recent-errors table below (time, source · error code, kind, * message). What an error needs to answer is "what exactly went wrong" — a * detail table is more direct than a chart here: the count alone in the stats already covers the summary. * * Color semantics are consistent site-wide: unexpected (500s / runtime * exceptions) is a prominent rose; expected (HttpError, business 4xx) recedes into gray. * The outer frame is provided by the caller's ChartCard (full width, below the four business charts). */ import type { UsageErrors } from "@prismshadow/penguin-server/api"; import { S } from "../../lib/strings"; import { formatDateTime } from "../../lib/format"; import { Badge } from "../../components/ui/badge"; import { Empty } from "./usage-charts"; /** The two error categories. */ type ErrorKindKey = "unexpected" | "expected"; /** Copy: S is a runtime live binding (switching language remounts the whole tree), so it must be read at render time. */ function kindLabel(key: ErrorKindKey): string { return key === "unexpected" ? S.usage.errorsUnexpected : S.usage.errorsExpected; } function kindOf(kind: string): ErrorKindKey { return kind === "unexpected" ? "unexpected" : "expected"; } /** A single small stat: name + value, one row side by side (not turned into a chart). */ function Stat({ label, value, alert, muted, }: { label: string; value: string; /** Prominent value (unexpected errors): rose. */ alert?: boolean; muted?: boolean; }) { const tone = alert ? "text-rose-600 dark:text-rose-400" : muted ? "text-gray-500 dark:text-gray-400" : "text-gray-900 dark:text-gray-100"; return (
{label} {value}
); } /** Header cell: left-aligned, recessive gray; stickiness is handled by thead. */ function Th({ children, className = "" }: { children: React.ReactNode; className?: string }) { return {children}; } /** * Error panel: stats + a recent-errors table (the server already takes the top N, newest first). * The table is table-fixed with in-cell truncation: a long message doesn't break the layout, and the full text goes into title. */ export function ErrorsPanel({ errors }: { errors: UsageErrors }) { const { total, unexpected, topCode, recent } = errors; return (
{/* Stats: a row of small stats (unexpected is prominent, expected recedes) */}
0} muted={unexpected === 0} /> {topCode && ( )}
{/* Recent-errors table */} {recent.length === 0 ? ( ) : (
{/* Wide enough to fully fit the longest error code: a tool failure's code carries the tool name (e.g. environment · tool_failed:exec_command), and truncating it would hide which tool failed. */} {recent.map((e, i) => { const key = kindOf(e.kind); return ( ); })}
{S.usage.errorsColTime}{S.usage.errorsColCode} {S.usage.errorsColKind} {S.usage.errorsColMessage}
{formatDateTime(e.ts)} {e.source} · {e.code} {kindLabel(key)} {e.message}
)}
); }