// --------------------------------------------------------------------------- // settings / platformAdminApi.ts — the Loopable admin plane's client half // (wave 19, owner item 13 / R3+R4, contract C2). // // ⛔ THE CLIENT HIDES; THE SERVER FORBIDS. Every route here is walled by // `core.platform_admin.is_platform_admin` — a double lock (the record flag AND // the `loopable` tenant), fail-closed, proven by `verify_api.py` enumerating the // router and having a tenant admin try every path. The `platformAdmin` flag on // `GET /settings` exists so the rail does not paint a door that would 403; it is // never the permission ([[aios-permissioning]]). // // SELF-CONTAINED BY CONTRACT (C2). This module and `AdminPane.tsx` are the only // two files the admin plane owns on the client, and they import nothing from the // rest of `settings/` — the pane fetches its own data, so mounting it is one line // in the rail and one line in the pane switch. // // ⚠ NULL IS A REAL VALUE HERE, AND IT IS NOT ZERO. Any per-tenant count can come // back `null`, meaning "that subsystem could not be read for this customer" — a // suspended tenant record, a locked keychain, an HF repo that did not answer. // Rendering it as 0 would tell an operator a customer has no databases when the // truth is that we did not look. The types say `number | null` for exactly that // reason and the pane renders the difference. // --------------------------------------------------------------------------- import { API_V1, CREDENTIALS } from "../apiContract"; export type ApiResult = { ok: true; data: T } | { ok: false; status: number; message: string }; async function call(path: string): Promise> { try { const res = await fetch(`${API_V1}/platform-admin${path}`, { credentials: CREDENTIALS }); let body: unknown = null; try { body = await res.json(); } catch { /* an unreadable body is handled below, never thrown at the pane */ } if (!res.ok) { const err = (body as { error?: { message?: string } } | null)?.error; const message = res.status >= 500 ? "Something went wrong on our side. Try again in a moment." : err?.message || (res.status === 403 ? "This surface is not available to your account." : "That could not be loaded."); return { ok: false, status: res.status, message }; } return { ok: true, data: body as T }; } catch { return { ok: false, status: 0, message: "The server could not be reached." }; } } /** One customer. `error` / `errors` carry the honest reason a count is null. */ export interface TenantRow { slug: string; name: string; /** `record` = provisioned into the control-plane bucket; `compiled` = tenant #0. */ source: string; status: string; domains: string[]; modules: string[] | "all"; /** R2: a dedicated dataset repo, or the shared repo. The isolation shape. */ storeRepo: string; storePrefix: string; error: string; users: number; admins: number; databases: number | null; rows: number | null; connectors: number | null; connectorsPaused?: number | null; automations: number | null; automationsEnabled?: number | null; keychainLocked?: boolean | null; errors?: string[]; } export interface Overview { tenants: TenantRow[]; totals: { tenants: number; users: number; orphanUsers: number; databases: number; rows: number; automations: number; unknownTenants: number; }; storeAvailable: boolean; generatedAt: string; tookMs: number; } export interface PlatformUser { username: string; name: string; email: string; tenant: string; role: string; active: boolean; /** R4 — absent means never, and the pane says "never" rather than inventing a date. */ lastLogin: string; lastActive: string; platformAdmin: boolean; } export interface DatabaseRow { tenant: string; key: string; label: string; source: string; createdBy: string; created: string; fields: number; rowCount: number; } export interface ConnectorRow { tenant: string; key: string; label: string; type: string; source: string; active: boolean; paused: boolean; } export interface AutomationRow { tenant: string; id: string; name: string; kind: string; enabled: boolean; cron: string; /** null = a cadence the server would not parse; shown as "custom", never as a number. */ runsPerDay: number | null; state: string; lastRunAt: string; lastSummary: string; runsRetained: number; failedRetained: number; createdBy: string; created: string; } export interface FleetCost { cadence: string; invocationsPerMonth: number; freeRequestsPct: number; freeSchedulerPct: number; lambdaMb: number; usd: number; fleetRunsPerDay: number; unknownCadence: number; basis: string; } export interface AwsReport { available: boolean; text: string; note: string; } export function getOverview(): Promise> { return call("/overview"); } /** `tenant` narrows every drill; omitted, they answer for the whole platform. */ const q = (tenant?: string) => (tenant ? `?tenant=${encodeURIComponent(tenant)}` : ""); export function getUsers( tenant?: string ): Promise> { return call(`/users${q(tenant)}`); } export function getDatabases( tenant?: string ): Promise< ApiResult<{ databases: DatabaseRow[]; count: number; rows: number; errors: Record; }> > { return call(`/databases${q(tenant)}`); } export function getConnectors( tenant?: string ): Promise< ApiResult<{ connectors: ConnectorRow[]; count: number; keychainLocked: Record; errors: Record; note: string; }> > { return call(`/connectors${q(tenant)}`); } export function getAutomations( tenant?: string ): Promise< ApiResult<{ automations: AutomationRow[]; count: number; enabled: number; historyRetained: number; cost: FleetCost; errors: Record; tickEnabled: boolean; }> > { return call(`/automations${q(tenant)}`); } export function getAws(days = 7): Promise> { return call(`/aws?days=${encodeURIComponent(String(days))}`); } // ── Wave 20 (owner item 12 / R6): RELEASES ────────────────────────────────── // Read-only by ruling. The operator SEES what is running where and what could be // gone back to; promoting stays `deploy_web.py --promote=vN`, so no web session // can move production. `promote` carries that command in the payload because the // person reading this panel is exactly the person who needs it. /** One environment. `version` is what the SPACE says about itself (its own * `VERSION` file) — never inferred from a timestamp, because `last_modified` * moves when a SECRET is pushed and would report a deploy that never happened. */ export interface ReleaseEnv { env: string; space: string; /** null + a `note` = we could not read it. NOT "nothing is deployed". */ version: string | null; note: string | null; /** HF runtime stage (`RUNNING`, `BUILDING`, …), or null when unreadable. */ stage: string | null; } export interface ReleaseTag { version: string; sha: string; date: string; subject: string; } export interface ReleasesPayload { /** The version THIS container is running (`AIOS_VERSION`), for the "you are here" case. */ here: string; environments: ReleaseEnv[]; releases: ReleaseTag[]; /** The CLI that moves a version. In the payload because the panel is read-only by ruling. */ promote: string; } export function getReleases(): Promise> { return call("/releases"); }