// — 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 { Badge, type BadgeTone } from "@hive/shared/badge.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 { 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, sortBy: (h) => h.name, filterValue: (h) => h.name, }, { key: "domain", header: "domain", render: (h) => h.domain ? ( {h.domain} ) : ( "—" ), sortBy: (h) => h.domain ?? "", filterValue: (h) => h.domain ?? "", }, { key: "status", header: "status", render: (h) => { const { tone, label } = FRESHNESS[h.freshness]; return ( {label} () ) : ( label ) } /> ); }, sortBy: (h) => FRESHNESS[h.freshness].label, // Multiselect (not the single-value `"select"` mode) so the operator // can filter for e.g. "stale or offline" in one pass rather than one // freshness state at a time. filterValues: (h) => [FRESHNESS[h.freshness].label], filterMode: "multiselect", }, ]; // 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} storageKey="swarm-ui:hives:table-filters" /> ) : null} ); }