SPB Git forge

spb/spinza

Public
8commits 1branches 0releases
1.6 MBsize
maindefault branch
16 days agolast push
TypeScript 97.6% SQL 1.4% JavaScript 0.5%
5.9 KB · 97 lines tsx
Raw Blame History
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