// --------------------------------------------------------------------------- // settings / AdminPane.tsx — THE LOOPABLE ADMIN PLANE (wave 19, owner item 13 / // R3+R4, contract C2). // // The one surface in this product that looks ACROSS tenants: who our customers // are, how many people use each workspace, what they have built in it, whether // their data sources are alive, and what the automation fleet costs. // // SELF-CONTAINED BY CONTRACT (C2). It fetches its own data via // `platformAdminApi` and takes ONE prop, so mounting it is a rail entry plus a // line in the pane switch — `SettingsModal.tsx` is another session's file this // wave and must not have to learn anything about this pane's data. // // ⚠ THE PROP IS TYPED STRUCTURALLY, not as `SessionUser` from `../shell/session`. // The mount site passes the shell's session user (a wider object), which // satisfies this shape by structural typing — and this pane therefore imports // nothing from the shell, which is what the wave's cross-fence rule asks for // while `session.ts` is open on another desk. // // ⛔ EVERY NUMBER HERE IS THE SERVER'S, AND EVERY NUMBER DRILLS. Clicking a count // opens the rows it was computed from — the same collector, projected twice // ([[no-unverifiable-aggregates]]). A count that could not be READ renders as an // em dash with the reason, never as a zero: "this customer has no databases" and // "we could not look" are different facts and the pane refuses to conflate them. // // DESIGN: sentence-case micro-labels (R6 — no caps in app chrome), short noun // headers, tabular figures right-aligned, hairlines not shadows, tokens only, // no emojis (DESIGN.md §2–§4). // --------------------------------------------------------------------------- import { useCallback, useEffect, useState } from "react"; import type { AutomationRow, ConnectorRow, DatabaseRow, FleetCost, Overview, PlatformUser, ReleasesPayload, } from "./platformAdminApi"; import { getAutomations, getAws, getReleases, getConnectors, getDatabases, getOverview, getUsers, } from "./platformAdminApi"; // --- formatting ------------------------------------------------------------- const int = (n: number) => n.toLocaleString(); /** A count that may be unknown. `null` is NOT zero — see the api module's header. */ function Count({ n, why }: { n: number | null | undefined; why?: string }) { if (n === null || n === undefined) return ( ); return <>{int(n)}; } /** "3 days ago" for a stamp, "never" for an absent one. Never a fabricated date. * * ⛔ ONLY FOR OFFSET-BEARING STAMPS. `Date.parse` reads a stamp with no zone as * BROWSER-LOCAL, so running a container-local timestamp through this reports the * viewer's UTC offset as elapsed time — hours of error in a column people scan, * and a future-dated stamp that renders as "just now" indefinitely. Everything * this pane passes here (`last_login`, `last_active`, `generatedAt`) is written * with an explicit offset; the automation engine's `lastRunAt` is NOT, so it is * rendered verbatim instead. */ function ago(stamp: string): string { if (!stamp) return "never"; const t = Date.parse(stamp.includes("T") ? stamp : stamp.replace(" ", "T")); if (Number.isNaN(t)) return stamp; const mins = Math.floor((Date.now() - t) / 60000); if (mins < 2) return "just now"; if (mins < 60) return `${mins} minutes ago`; const hours = Math.floor(mins / 60); if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`; const days = Math.floor(hours / 24); if (days < 60) return `${days} day${days === 1 ? "" : "s"} ago`; return `${Math.floor(days / 30)} months ago`; } type DrillKind = "users" | "databases" | "connectors" | "automations"; const DRILL_NOUN: Record = { users: "People", databases: "Databases", connectors: "Data sources", automations: "Automations", }; /** The loading shape — a skeleton, not a spinner (DESIGN.md §4). */ function Skeleton({ rows = 3 }: { rows?: number }) { return (