// — the hive roster, `/`'s content. Fetches // swarm-controller's `GET /api/hives/status`, the swarm-wide status // aggregate: one row per roster hive, freshness derived at read time // from the status bucket. Supersedes the earlier `GET /api/hives` + // static "configured" chip placeholder now that the real rollup // exists — same component, richer data, no rebuild, exactly the plan // that placeholder's comment laid out. Its own component/file rather // than living in `App.tsx`, matching `JobsPage`'s shape: `App.tsx` is // routing, a page owns its own fetch + render. // // Polls on a `RefreshIntervalPicker` cadence rather than fetching once // at mount — no inputs on this page, so a re-fetch clobbering an // in-progress edit (the hook's stated caller obligation) isn't a live // concern here. import { useState } from 'preact/hooks'; import { ApiErrorPanel } from '@hive/shared/api-error-panel.js'; import { readApiError, type ProblemDetails } from '@hive/shared/api-error.js'; import { Panel } from '../ui/panel/Panel.js'; import { RelativeTime } from '../ui/relative-time/RelativeTime.js'; import { RefreshIntervalPicker, useRefreshInterval, type RefreshIntervalMs, } from '../ui/refresh-interval/RefreshInterval.js'; import { StatusChip, type ChipTone } from '../ui/status-chip/StatusChip.js'; import { Table, type TableColumn } from '../ui/table/Table.js'; type Freshness = 'fresh' | 'stale' | 'never_reported' | 'unknown'; interface HiveStatus { name: string; domain: string | null; freshness: Freshness; last_seen_unix: number | null; // `age_seconds` (a once-computed-at-fetch age) is part of the wire // shape but deliberately unused here — rendering it directly is the // exact bug `RelativeTime` exists to fix, so this page recomputes the // age client-side from `last_seen_unix` instead. } // One tone + label per freshness value. `unknown` (reporting but absent // from the roster) reads `negative` — it's the one case this endpoint // cannot vouch for at all, per `status.rs`'s doc comment. const FRESHNESS: Record = { fresh: { tone: 'positive', label: 'fresh' }, stale: { tone: 'warning', label: 'stale' }, never_reported: { tone: 'neutral', label: 'never reported' }, unknown: { tone: 'negative', label: 'unknown' }, }; const COLUMNS: TableColumn[] = [ { key: 'name', header: 'name', render: (h) => h.name }, { key: 'domain', header: 'domain', render: (h) => h.domain ? ( {h.domain} ) : ( '—' ), }, { key: 'status', header: 'status', render: (h) => { const { tone, label } = FRESHNESS[h.freshness]; return ( {label} () ) : ( label ) } /> ); }, }, ]; // 30s default: this page has no inputs to interrupt, and the whole // point of a refresh-interval control is "no manual reload needed" — an // operator who wants it off still can, but the out-of-the-box behaviour // should actually solve the staleness problem rather than require an // opt-in every visit. const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000; export function HivesPage() { const [hives, setHives] = useState(null); const [error, setError] = useState(null); const [intervalMs, setIntervalMs] = useState(DEFAULT_INTERVAL_MS); useRefreshInterval(intervalMs, () => { (async () => { const r = await fetch('/api/hives/status'); if (!r.ok) { setError(await readApiError(r)); return; } setHives((await r.json()) as HiveStatus[]); // A refresh that succeeds clears a previous failure — otherwise a // transient error would sit on screen forever after the data // itself has recovered. setError(null); })().catch((e: unknown) => setError({ detail: String(e) })); }); return ( {error ? : null} {!error && hives === null ?

loading…

: null} {hives ? h.name} /> : null} ); }