hyperhive/frontend/packages/swarm-ui/src/App.tsx
iris e7f4a19939 swarm-ui: real hive-roster overview page
Fixes hyperhive#3223.

swarm-controller: GET /api/hives (utoipa-annotated same as /health),
serving the swarm's hive directory (name + domain) loaded once at
startup from a new SWARM_CONTROLLER_HIVES env var. The controller's
NixOS module sets it from services.hyperhive.swarm.hives, JSON-encoded
the same way hive-c0re already builds HYPERHIVE_PEERS for its own peer
list (environment.nix) — the full directory here rather than
peers-minus-self, since a swarm-level daemon has no 'self' hive to
exclude. Unset/malformed both fall back to an empty list with a
warning rather than failing startup, so /health stays answerable even
if this one env var is wrong.

swarm-ui: App.tsx's Home route fetches /api/hives and renders it
through the already-merged <Table>/<StatusChip>/<Panel> primitives —
name, domain (linking out to that hive's own gateway-routed
dashboard), and a static "configured" status chip until a real
online/stale/offline rollup exists server-side. Also gave swarm-ui a
base <a> color (theme's --blue) — base.css covers body/typography but
not links, and this is genuinely page-level rather than any one
component's concern.

Verified end to end, not just source-reading: ran the real
swarm-controller binary with SWARM_CONTROLLER_HIVES set, curled
/api/hives + /health over its actual unix socket; separately served
the real swarm-ui dist against a mock /api/hives and screenshotted the
rendered table. Also re-verified the nginx wiring evaluates (same
throwaway nixosSystem eval technique as #3212) — SWARM_CONTROLLER_HIVES
resolves to the expected JSON shape.

cargo test/clippy -p swarm-controller clean (2 tests, including a new
load_hives one covering missing/malformed/valid env var states). npm
run build + typecheck clean.
2026-08-13 11:21:30 +02:00

76 lines
2.3 KiB
TypeScript

// Root shell component. `/` is now the real hive-roster overview page —
// fetches swarm-controller's `GET /api/hives` and renders it through the
// shared primitives. Status starts as a static "configured" chip; grows
// into a real online/stale/offline tone once a status rollup exists
// server-side — same component, richer data later, no rebuild.
import { useEffect, useState } from 'preact/hooks';
import { Route, Switch } from 'wouter-preact';
import { Shell } from './shell/Shell.js';
import { Panel } from './ui/panel/Panel.js';
import { StatusChip } from './ui/status-chip/StatusChip.js';
import { Table, type TableColumn } from './ui/table/Table.js';
interface Hive {
name: string;
domain: string;
}
const COLUMNS: TableColumn<Hive>[] = [
{ key: 'name', header: 'name', render: (h) => h.name },
{
key: 'domain',
header: 'domain',
render: (h) => (
<a href={`https://${h.domain}/`} target="_blank" rel="noreferrer">
{h.domain}
</a>
),
},
// Static "configured" tone until a swarm-wide status rollup exists —
// there is no data source for online/stale/offline yet, and a chip
// that always renders "positive" would misreport a genuinely offline
// hive. See StatusChip's own doc comment for the same reasoning.
{ key: 'status', header: 'status', render: () => <StatusChip label="configured" /> },
];
function Home() {
const [hives, setHives] = useState<Hive[] | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch('/api/hives')
.then((r) => {
if (!r.ok) throw new Error(`http ${r.status}`);
return r.json() as Promise<Hive[]>;
})
.then(setHives)
.catch((e: unknown) => setError(String(e)));
}, []);
return (
<Panel title="overview">
{error ? <p>failed to load the hive roster: {error}</p> : null}
{!error && hives === null ? <p>loading</p> : null}
{hives ? <Table columns={COLUMNS} rows={hives} rowKey={(h) => h.name} /> : null}
</Panel>
);
}
function NotFound() {
return (
<Panel title="404">
<p>no route here.</p>
</Panel>
);
}
export function App() {
return (
<Shell>
<Switch>
<Route path="/" component={Home} />
<Route component={NotFound} />
</Switch>
</Shell>
);
}