From 209a286d7c7e5179304fd5353ddabf6e5852398d Mon Sep 17 00:00:00 2001 From: iris Date: Mon, 26 Oct 2026 16:51:14 +0100 Subject: [PATCH] swarm-ui: repoint the HivesPage comments at HivesCard and pin the nav filter and lazy roster --- .../packages/swarm-ui/src/agent/AgentTypes.ts | 6 +++--- .../packages/swarm-ui/src/pages/AlertsPage.tsx | 2 +- .../swarm-ui/src/pages/ComponentsPage.tsx | 4 ++-- .../swarm-ui/src/pages/CreateAgentForm.tsx | 2 +- .../packages/swarm-ui/src/pages/JobsPage.tsx | 4 ++-- .../swarm-ui/src/pages/agents/AgentsPage.tsx | 4 ++-- .../swarm-ui/src/pages/agents/HivesCard.tsx | 3 ++- .../swarm-ui/src/pages/agents/lazy-open.ts | 5 +++++ frontend/packages/swarm-ui/src/shell/Shell.tsx | 6 ++---- frontend/packages/swarm-ui/src/shell/routes.ts | 12 ++++++++++++ .../packages/swarm-ui/src/ui/panel/Panel.tsx | 2 +- .../packages/swarm-ui/test/lazy-open.test.mjs | 16 ++++++++++++++++ frontend/packages/swarm-ui/test/routes.test.mjs | 15 ++++++++++++++- 13 files changed, 63 insertions(+), 18 deletions(-) create mode 100644 frontend/packages/swarm-ui/src/pages/agents/lazy-open.ts create mode 100644 frontend/packages/swarm-ui/test/lazy-open.test.mjs diff --git a/frontend/packages/swarm-ui/src/agent/AgentTypes.ts b/frontend/packages/swarm-ui/src/agent/AgentTypes.ts index dc82f7d1..dbcc29d4 100644 --- a/frontend/packages/swarm-ui/src/agent/AgentTypes.ts +++ b/frontend/packages/swarm-ui/src/agent/AgentTypes.ts @@ -32,7 +32,7 @@ export interface AgentRow { name: string; hive: string | null; freshness: Freshness; - // `age_seconds` deliberately unused here, same reason as HivesPage: + // `age_seconds` deliberately unused here, same reason as HivesCard: // `RelativeTime` recomputes age client-side from `last_seen_unix` // rather than rendering a once-computed-at-fetch value. last_seen_unix: number | null; @@ -50,11 +50,11 @@ export function showsLoginNeeded(row: Pick): boolean { return row.needs_login === true; } -// Same tone pairing as HivesPage — one freshness enum shared by both +// Same tone pairing as HivesCard — one freshness enum shared by both // endpoints, so the same tone rule applies to both pages — but // `unknown`'s label diverges deliberately: an agent outside the // swarm-identity roster has a concrete next step (register it), while -// an unrecognized *hive* on HivesPage doesn't carry the same +// an unrecognized *hive* on HivesCard doesn't carry the same // actionable meaning. export const FRESHNESS: Record = { diff --git a/frontend/packages/swarm-ui/src/pages/AlertsPage.tsx b/frontend/packages/swarm-ui/src/pages/AlertsPage.tsx index 42bd0b1e..f2e77dc8 100644 --- a/frontend/packages/swarm-ui/src/pages/AlertsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/AlertsPage.tsx @@ -1,7 +1,7 @@ // — `/alerts`' content: the alerts the swarm's Grafana holds // now, from swarm-controller's `GET /api/alerts`. Only the firing ones show at // first; a button in the header adds the pending, no-data and errored ones. -// Same shape as `HivesPage`: one fetch on a `RefreshIntervalPicker` cadence, +// Same shape as `HivesCard`: one fetch on a `RefreshIntervalPicker` cadence, // rendered as a `Table`. import { useState } from "preact/hooks"; import { ApiErrorPanel } from "@hive/shared/api-error-panel.js"; diff --git a/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx b/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx index 33202b6f..ddea62a3 100644 --- a/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx @@ -80,7 +80,7 @@ const TABLE_COLUMNS: TableColumn[] = [ render: (r) => r.detail, // One filterable column, multiselect mode — the checkbox-list popover // + reset-filters button are both real `Table` behavior an operator - // hits daily (`AgentsPage`/`HivesPage`), so the demo shows them rather + // hits daily (`AgentsPage`/`HivesCard`), so the demo shows them rather // than a plain unfilterable grid. filterValues: (r) => [r.detail], filterMode: "multiselect", @@ -408,7 +408,7 @@ export function ComponentsPage() { panel body content - + { setOpen(next); - if (next) setBuilt(true); + setBuilt((was) => builtAfterToggle(was, next)); }} > {built ? : null} diff --git a/frontend/packages/swarm-ui/src/pages/agents/lazy-open.ts b/frontend/packages/swarm-ui/src/pages/agents/lazy-open.ts new file mode 100644 index 00000000..943d0d3b --- /dev/null +++ b/frontend/packages/swarm-ui/src/pages/agents/lazy-open.ts @@ -0,0 +1,5 @@ +// A folded card builds its body on the first open and keeps it after, so a +// card nobody opens never fetches, and closing it does not drop what it read. +export function builtAfterToggle(built: boolean, open: boolean): boolean { + return built || open; +} diff --git a/frontend/packages/swarm-ui/src/shell/Shell.tsx b/frontend/packages/swarm-ui/src/shell/Shell.tsx index cee67fb6..45964184 100644 --- a/frontend/packages/swarm-ui/src/shell/Shell.tsx +++ b/frontend/packages/swarm-ui/src/shell/Shell.tsx @@ -36,7 +36,7 @@ import { SettingsMenu } from "@hive/shared/settings-menu.js"; import { HideDebugSetting } from "@hive/shared/hide-debug-setting.js"; import { useApplyThemeOverride } from "@hive/shared/theme-apply.js"; import { useApplyMotionOverride } from "@hive/shared/motion-apply.js"; -import { isFullBleedRoute } from "./routes.js"; +import { isFullBleedRoute, visibleNavItems } from "./routes.js"; import { useDynamicTabs } from "./useDynamicTabs.js"; import { useSwarmName } from "./useSwarmName.js"; import "./Shell.css"; @@ -212,9 +212,7 @@ export function Shell({ children }: { children: ComponentChildren }) { // The components tab is a developer page: it goes while "hide developer // details" is on. The page itself still answers at `/components`. const hideDeveloper = useHideDebug(); - const navItems = hideDeveloper - ? NAV_ITEMS.filter((item) => item.href !== "/components") - : NAV_ITEMS; + const navItems = visibleNavItems(NAV_ITEMS, hideDeveloper); // Applied once here, not inside `SettingsMenu` — every route mounts // through this one ``, so the override takes effect regardless diff --git a/frontend/packages/swarm-ui/src/shell/routes.ts b/frontend/packages/swarm-ui/src/shell/routes.ts index 0c3d793a..57d17296 100644 --- a/frontend/packages/swarm-ui/src/shell/routes.ts +++ b/frontend/packages/swarm-ui/src/shell/routes.ts @@ -15,3 +15,15 @@ const FULL_BLEED_ROUTE_PATTERN = export function isFullBleedRoute(location: string): boolean { return location === "/" || FULL_BLEED_ROUTE_PATTERN.test(location); } + +// Routes that only a developer needs. "hide developer details" takes their +// tabs out of the nav; the pages themselves still answer at their paths. +const DEVELOPER_ROUTES = ["/components"]; + +export function visibleNavItems( + items: readonly T[], + hideDeveloper: boolean, +): readonly T[] { + if (!hideDeveloper) return items; + return items.filter((item) => !DEVELOPER_ROUTES.includes(item.href)); +} diff --git a/frontend/packages/swarm-ui/src/ui/panel/Panel.tsx b/frontend/packages/swarm-ui/src/ui/panel/Panel.tsx index b1aa05fd..9566deaf 100644 --- a/frontend/packages/swarm-ui/src/ui/panel/Panel.tsx +++ b/frontend/packages/swarm-ui/src/ui/panel/Panel.tsx @@ -9,7 +9,7 @@ // belongs next to the thing it affects, not tucked into a catch-all // menu" — and per mara's review on the refresh-interval PR, not a // dedicated row stealing vertical space from the panel's actual -// content either). `HivesPage`'s refresh-interval picker is the +// content either). `HivesCard`'s refresh-interval picker is the // motivating caller. // // `onClose`: end-of-header close (✕) for `Dialog`'s `plain` mode. diff --git a/frontend/packages/swarm-ui/test/lazy-open.test.mjs b/frontend/packages/swarm-ui/test/lazy-open.test.mjs new file mode 100644 index 00000000..8fa8ac31 --- /dev/null +++ b/frontend/packages/swarm-ui/test/lazy-open.test.mjs @@ -0,0 +1,16 @@ +// A folded card fetches nothing until first opened. Run by `npm test`. +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { builtAfterToggle } from "../src/pages/agents/lazy-open.ts"; + +test("a card nobody opened is not built", () => { + assert.equal(builtAfterToggle(false, false), false); +}); + +test("the first open builds it", () => { + assert.equal(builtAfterToggle(false, true), true); +}); + +test("closing it afterwards keeps it built", () => { + assert.equal(builtAfterToggle(true, false), true); +}); diff --git a/frontend/packages/swarm-ui/test/routes.test.mjs b/frontend/packages/swarm-ui/test/routes.test.mjs index 77b1add1..a77b80b5 100644 --- a/frontend/packages/swarm-ui/test/routes.test.mjs +++ b/frontend/packages/swarm-ui/test/routes.test.mjs @@ -1,7 +1,7 @@ // The shell's per-route layout: which paths are full-bleed. Run by `npm test`. import assert from "node:assert/strict"; import { test } from "node:test"; -import { isFullBleedRoute } from "../src/shell/routes.ts"; +import { isFullBleedRoute, visibleNavItems } from "../src/shell/routes.ts"; test("the start page is full-bleed", () => { assert.equal(isFullBleedRoute("/"), true); @@ -29,3 +29,16 @@ test("a terminal path with extra segments is not full-bleed", () => { assert.equal(isFullBleedRoute("/agents/iris/terminal/extra"), false); assert.equal(isFullBleedRoute("/agents//terminal"), false); }); + +const NAV = [{ href: "/agents" }, { href: "/components" }, { href: "/jobs" }]; + +test("hide developer details takes the components tab out of the nav", () => { + assert.deepEqual( + visibleNavItems(NAV, true).map((i) => i.href), + ["/agents", "/jobs"], + ); +}); + +test("with it off the nav is whole again", () => { + assert.deepEqual(visibleNavItems(NAV, false), NAV); +});