TypeScript 97.6%
SQL 1.4%
JavaScript 0.5%
1"use client";23import { Cpu, Database, MemoryStick, Server } from "lucide-react";4import { useAdminQuery } from "@/components/admin/use-query";5import type { SystemResponse } from "@/components/admin/types";6import { PageHeader, RefreshButton, Panel, DataTable, ErrorState, TableSkeleton, TileSkeleton, StatGrid, StatTile, KV, Pill, Mono, type Column } from "@/components/admin/primitives";7import { bytes, duration, int, ms, num } from "@/components/admin/format";8import { cn } from "@/lib/utils";910type Service = SystemResponse["services"][number];11type TableRow = SystemResponse["tables"][number] & { __max: number };1213const serviceCols: Column<Service>[] = [14 { key: "status", header: "Status", render: (s) => <Pill tone={s.status === "ok" ? "success" : "danger"} dot>{s.status}</Pill> },15 { key: "name", header: "Service", render: (s) => <span className="font-medium text-fg">{s.name}</span> },16 { key: "node", header: "Node", mono: true, render: (s) => <Mono>{s.node}</Mono> },17 { key: "version", header: "Version", mono: true, render: (s) => <Mono>v{s.version}</Mono> },18 { key: "uptime", header: "Uptime", align: "right", render: (s) => (s.uptimeSec === null ? <span className="text-fg-4">—</span> : duration(s.uptimeSec)) },19 { key: "latency", header: "Latency", align: "right", render: (s) => (s.latencyMs < 0 ? <span className="text-danger">unreachable</span> : ms(s.latencyMs, 1)) },20];2122const tableCols: Column<TableRow>[] = [23 { key: "table", header: "Table", mono: true, render: (t) => <Mono className="text-fg">{t.table}</Mono> },24 { key: "rows", header: "Live rows", align: "right", render: (t) => int(t.rows) },25 { key: "bytes", header: "Total size", align: "right", render: (t) => bytes(t.bytes) },26 {27 key: "share",28 header: "",29 render: (t) => <span className="block h-1.5 w-full max-w-[160px] overflow-hidden rounded-full bg-surface-3"><span className="block h-full rounded-full bg-accent/70" style={{ width: `${Math.min(100, (num(t.bytes) / num(t.__max)) * 100)}%` }} /></span>,30 },31];3233export default function AdminSystemPage() {34 const q = useAdminQuery<SystemResponse>("/api/admin/system", { refreshMs: 15_000 });35 const d = q.data;36 const maxBytes = d ? Math.max(1, ...d.tables.map((t) => num(t.bytes))) : 1;37 const tables = (d?.tables ?? []).map((t) => ({ ...t, __max: maxBytes }));38 const down = d?.services.filter((s) => s.status !== "ok").length ?? 0;39 const memPct = d ? 1 - d.process.freeMemMb / Math.max(1, d.process.totalMemMb) : 0;4041 return (42 <>43 <PageHeader title="System Health" description="API process, dependencies and storage. Refreshes every 15 seconds." actions={<RefreshButton onClick={() => void q.refresh()} loading={q.refreshing} />} />44 {q.error && !d ? (45 <ErrorState error={q.error} onRetry={() => void q.refresh()} />46 ) : !d ? (47 <div className="space-y-4">48 <TileSkeleton count={4} cols={4} />49 <Panel>50 <TableSkeleton rows={3} cols={6} />51 </Panel>52 </div>53 ) : (54 <div className={cn("space-y-4", q.stale && "opacity-70")}>55 <StatGrid cols={4}>56 <StatTile label="Services" value={down === 0 ? "All healthy" : `${down} down`} tone={down === 0 ? "success" : "danger"} sub={`${d.services.length} monitored · ${d.games} games loaded`} icon={<Server className="h-3.5 w-3.5" />} />57 <StatTile label="Spin latency · 1 h" value={<span>{ms(d.spins.avg)} <span className="text-fg-4">avg</span></span>} sub={`p95 ${ms(d.spins.p95)} · n=${int(d.spins.n)}`} tone={num(d.spins.p95) > 250 ? "danger" : "neutral"} />58 <StatTile label="Process memory" value={`${int(d.process.rssMb)} MB`} sub={`heap ${int(d.process.heapMb)} MB · Node ${d.process.nodeVersion}`} icon={<MemoryStick className="h-3.5 w-3.5" />} />59 <StatTile label="Load average" value={d.process.load.map((l) => l.toFixed(2)).join(" / ")} sub={`${d.process.cpus} CPUs · ${d.process.platform}`} tone={d.process.load[0] > d.process.cpus ? "danger" : "neutral"} icon={<Cpu className="h-3.5 w-3.5" />} />60 </StatGrid>6162 <Panel title="Services" padded={false}>63 <DataTable columns={serviceCols} rows={d.services} rowKey={(s) => s.name} dense />64 </Panel>6566 <div className="grid gap-4 xl:grid-cols-[1fr_1.4fr]">67 <Panel title="Host" description="Node running the API process">68 <KV69 items={[70 { label: "Platform", value: d.process.platform, mono: true },71 { label: "Node.js", value: d.process.nodeVersion, mono: true },72 { label: "CPUs", value: String(d.process.cpus) },73 { label: "Load 1 / 5 / 15", value: d.process.load.map((l) => l.toFixed(2)).join(" / "), mono: true },74 { label: "Memory used", value: `${int(d.process.totalMemMb - d.process.freeMemMb)} / ${int(d.process.totalMemMb)} MB` },75 { label: "RSS / heap", value: `${int(d.process.rssMb)} / ${int(d.process.heapMb)} MB` },76 ]}77 />78 <div className="mt-4">79 <div className="mb-1 flex justify-between text-[11px] text-fg-4">80 <span>Host memory</span>81 <span>{Math.round(memPct * 100)}%</span>82 </div>83 <div className="h-1.5 w-full overflow-hidden rounded-full bg-surface-3">84 <div className={cn("h-full rounded-full", memPct > 0.9 ? "bg-danger" : memPct > 0.75 ? "bg-[#ffb454]" : "bg-accent")} style={{ width: `${Math.round(memPct * 100)}%` }} />85 </div>86 </div>87 </Panel>88 <Panel title="Largest tables" description="pg_stat_user_tables, top 15 by total relation size" padded={false} actions={<Database className="h-3.5 w-3.5 text-fg-4" />}>89 <DataTable columns={tableCols} rows={tables} rowKey={(t) => t.table} dense empty="No table statistics available." />90 </Panel>91 </div>92 </div>93 )}94 </>95 );96}97