diff --git a/docs/swarm/notify-router.md b/docs/swarm/notify-router.md index 2feebddf..e8540cf2 100644 --- a/docs/swarm/notify-router.md +++ b/docs/swarm/notify-router.md @@ -83,7 +83,7 @@ todo, and `head-classifier`: source `pull_request` and `pull_request_sync` → match on `action` (`opened`, `reopened`, `synchronized`) and `draft` = `false`, or on `undrafted` → `request_reviewer` for `review-classifier` ([below](#requesting-a-review)). The code defines them (`swarm_wire::notify_flows::builtins`), so a build that changes one reaches -every swarm. Each switches off on `/notifications`. A custom flow has the same type and +every swarm. Each switches off on the notification router card on `/swarm`. A custom flow has the same type and the same evaluator, so it can do anything a built-in does. **The stored document.** swarm-controller keeps the rest in one document, the @@ -96,7 +96,7 @@ check above and reads only what exists: a source event from `issues`, three `pull_request_review_*` events, and condition and recipient fields from the event's field list. The same goes for the `{field}` placeholders in a todo's key and text. A `PUT` must carry the `revision` it read, `0` for no document, and gets 409 when the stored revision is another. Swarm UI's -[`/notifications`](ui.md#notification-flows) page shows the document and +[notification router card on `/swarm`](ui.md#notification-flows) shows the document and switches built-ins. **The schema.** `GET /api/notify/flows` also returns a `schema` object, built @@ -263,7 +263,7 @@ serves them next to `GET /api/notify/flows`: The dry-run request carries a whole document. The `GET` sends the saved one; the `POST` takes a `PUT` body (it ignores a `revision`), answers 400 with the `PUT` error for a document that fails the check, and forwards a valid one. -[`/notifications`](ui.md#notification-flows) shows both: the recent deliveries, +[notification router card on `/swarm`](ui.md#notification-flows) shows both: the recent deliveries, and the counts beside each node. **Deploying in any order.** A router that predates these subjects doesn't diff --git a/docs/swarm/ui.md b/docs/swarm/ui.md index c390a092..472043fb 100644 --- a/docs/swarm/ui.md +++ b/docs/swarm/ui.md @@ -26,13 +26,13 @@ smaller; short confirm dialogs stay short. | route | what | | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `/` | the start page: the swarm's name over a digital-rain backdrop with the slug (`hyperhive / we are the wired`, its shaded gradients pulsing in a wave unless reduced motion is on) at the foot, plus a paperclip offering to set up an agent while no `ruth` exists, or her hive declares her destroyed (it opens the create form with `ruth` filled in); the swarm name in the header links here | -| `/agents` | every agent as a card that opens in place (status, terminal, subagents, and settings with permissions, internet, allowed domains and accounts), under a folded **hives** card with the hive directory and each hive's last reported status; create agents (with their permissions, optionally from an [archetype](archetypes.md)), link forge, matrix and GitHub accounts | +| `/agents` | every agent as a card that opens in place (status, terminal, subagents, and settings with permissions, internet, allowed domains and accounts); create agents (with their permissions, optionally from an [archetype](archetypes.md)), link forge, matrix and GitHub accounts | +| `/swarm` | what belongs to the whole swarm: a folded **hives** card with the hive directory and each hive's last reported status, and a folded, **beta** **notification router** card (with a warning banner) holding the router's flows: a switch for each built-in, every flow drawn as a graph, and an editor for custom flows. `/notifications` redirects here | | `/agents//terminal` | one agent's live terminal | | `/agents//subagents//terminal` | one of that agent's subagents' live terminal | | `/reports` | two folded cards, each read only once opened: the **alerts** the swarm's Grafana holds now, the firing ones, with a button for the pending, no-data and error ones and a link to Grafana's own alert list; read through the controller with a Viewer service-account token, and a cross-repo **issue report** | | `/jobs` | the controller's job graph, where agent creation and credential mints show progress, and under it the swarm builder's recent builds (agent config PRs, and agents built from `internal/meta` before a deploy), with what nix printed when one failed, and the review classifier's recent runs, a try-out on a real PR, and its settings | | `/messages` | the swarm-wide message feed: every message between agents, and to them from the operator, reminders and compaction, with a history window and a filter on sender or recipient; a folded **broadcast** card above the feed sends one message to more than one agent at once, and a folded **schedules** card holds the scheduled prompts: add, edit, pause, resume and remove | -| `/notifications` | the notification router's flows: a switch for each built-in, every flow drawn as a graph, and an editor for custom flows | Everything it shows comes from [`swarm-controller`](../../swarm-controller/README.md). Pages that refresh on a timer share one polling rule: the page sends no new @@ -58,7 +58,7 @@ works too. ### The agents page -`/agents` is a column of separate cards: the hives card, a filter card, then one +`/agents` is a column of separate cards: a filter card, then one card per agent, none of them inside a bigger card. A card's header shows its icon, name, freshness, the status message, a **terminal** button that opens the agent's terminal tab, the **wanted** menu (up, offline, @@ -295,7 +295,7 @@ routes. ### Notification flows -`/notifications` lists the [notification router's flows](notify-router.md#flows) +The **notification router** card on `/swarm` (beta, folded; `/notifications` redirects to it) lists the [notification router's flows](notify-router.md#flows) in two groups. Each **built-in** flow has an on/off switch; switching one off stops the router running it from its next event, and switching it on again restores it. Each **custom** flow shows its name and whether it's on. Every diff --git a/frontend/packages/swarm-ui/src/App.tsx b/frontend/packages/swarm-ui/src/App.tsx index 4afa9d16..f2cc42fb 100644 --- a/frontend/packages/swarm-ui/src/App.tsx +++ b/frontend/packages/swarm-ui/src/App.tsx @@ -9,7 +9,7 @@ import { AgentTerminalPage } from "./pages/agents/AgentTerminalPage.js"; import { MessagesPage } from "./pages/messages/MessagesPage.js"; import { NEW_FLOW_PATH } from "./pages/notifications/FlowDraft.js"; import { FlowEditorPage } from "./pages/notifications/FlowEditorPage.js"; -import { NotificationsPage } from "./pages/notifications/NotificationsPage.js"; +import { SwarmPage } from "./pages/swarm/SwarmPage.js"; import { ComponentsPage } from "./pages/ComponentsPage.js"; import { JobsPage } from "./pages/JobsPage.js"; import { ReportsPage } from "./pages/reports/ReportsPage.js"; @@ -50,7 +50,7 @@ export function App() { /> - + diff --git a/frontend/packages/swarm-ui/src/pages/agents/AgentsPage.css b/frontend/packages/swarm-ui/src/pages/agents/AgentsPage.css deleted file mode 100644 index 6097ce72..00000000 --- a/frontend/packages/swarm-ui/src/pages/agents/AgentsPage.css +++ /dev/null @@ -1,10 +0,0 @@ -/* The folded hives card over the agents. */ -.ui-agents-hives { - margin-block-end: 0.5em; -} -/* The roster's refresh picker, at the top right of the open card. */ -.ui-hives-refresh { - display: flex; - justify-content: flex-end; - margin-block-end: 0.5em; -} diff --git a/frontend/packages/swarm-ui/src/pages/agents/AgentsPage.tsx b/frontend/packages/swarm-ui/src/pages/agents/AgentsPage.tsx index 7c5ec19b..7b547161 100644 --- a/frontend/packages/swarm-ui/src/pages/agents/AgentsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/agents/AgentsPage.tsx @@ -4,7 +4,7 @@ // status panel has anything to render against without a roster to embed // in — so all three ended up as one page. // -// One fetch, on a refresh-interval cadence like HivesCard: `GET +// One fetch, on a refresh-interval cadence like the hives card: `GET // /api/agents/status` — one row per roster agent (identity store is the // roster, so a never-reported agent still gets a row), each already // carrying its freshness/snapshot/config PR/wanted state. Used to be @@ -36,7 +36,6 @@ import { FRESHNESS, type AgentRow } from "../../agent/AgentTypes.js"; import { Button } from "../../ui/button/Button.js"; import { Dialog } from "../../ui/dialog/Dialog.js"; import { FilterableView } from "../../ui/filterable-view/FilterableView.js"; -import { HivesCard } from "./HivesCard.js"; import { RefreshIntervalPicker, type RefreshIntervalMs, @@ -44,9 +43,8 @@ import { import { fetchJson, usePolled } from "../../ui/polled/usePolled.js"; import { type FilterColumn } from "../../ui/table/Table.js"; import { CreateAgentForm } from "../CreateAgentForm.js"; -import "./AgentsPage.css"; -// Same 30s default + same reasoning as HivesCard: no inputs on this page +// Same 30s default + same reasoning as the hives card: no inputs on this page // for a refresh to clobber, so the out-of-the-box behaviour should just // solve staleness rather than require an opt-in every visit. const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000; @@ -139,9 +137,6 @@ export function AgentsPage() { return ( <> -
- -
{/* Until the roster loads (or when it fails) there are no filters, but the buttons stay, so an agent can still be created. */} {rows === null ? ( diff --git a/frontend/packages/swarm-ui/src/pages/notifications/NotificationsPage.tsx b/frontend/packages/swarm-ui/src/pages/notifications/NotificationsPage.tsx index ef2c5026..d779977e 100644 --- a/frontend/packages/swarm-ui/src/pages/notifications/NotificationsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/notifications/NotificationsPage.tsx @@ -1,6 +1,7 @@ -// — the notification router's flows. Backed by -// swarm-controller's `GET/PUT /api/notify/flows`; the controller is the only -// writer of the stored document and the router reads it every few seconds. +// — the notification router's flows, the body of its card on +// the swarm page. Backed by swarm-controller's `GET/PUT /api/notify/flows`; the +// controller is the only writer of the stored document and the router reads it +// every few seconds. // // A flow draws as its graph, read-only. A built-in flow has a switch and can // be duplicated; a custom flow is edited on its own page (). @@ -13,7 +14,6 @@ import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js"; import { Button } from "../../ui/button/Button.js"; import { Card } from "../../ui/card/Card.js"; import { CheckboxField } from "../../ui/checkbox-field/CheckboxField.js"; -import { Panel } from "../../ui/panel/Panel.js"; import { RefreshIntervalPicker, type RefreshIntervalMs, @@ -35,7 +35,7 @@ import "./NotificationsPage.css"; const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000; -export function NotificationsPage() { +export function NotificationRouter() { const [intervalMs, setIntervalMs] = useState(DEFAULT_INTERVAL_MS); const { @@ -96,22 +96,17 @@ export function NotificationsPage() { ); return ( - - - - - } - > + <> +
+ + +
{error ? ( ) : null} @@ -192,6 +187,6 @@ export function NotificationsPage() { )} ) : null} -
+ ); } diff --git a/frontend/packages/swarm-ui/src/pages/agents/HivesCard.tsx b/frontend/packages/swarm-ui/src/pages/swarm/HivesCard.tsx similarity index 100% rename from frontend/packages/swarm-ui/src/pages/agents/HivesCard.tsx rename to frontend/packages/swarm-ui/src/pages/swarm/HivesCard.tsx diff --git a/frontend/packages/swarm-ui/src/pages/swarm/SwarmPage.css b/frontend/packages/swarm-ui/src/pages/swarm/SwarmPage.css new file mode 100644 index 00000000..4e5c46af --- /dev/null +++ b/frontend/packages/swarm-ui/src/pages/swarm/SwarmPage.css @@ -0,0 +1,19 @@ +/* The cards on the swarm page: hives, then the notification router. */ +.ui-swarm-card { + margin-block-end: 0.5em; +} +/* The roster's refresh picker, at the top right of the open card. */ +.ui-hives-refresh { + display: flex; + justify-content: flex-end; + margin-block-end: 0.5em; +} +/* The warning at the top of the beta notification router card. */ +.ui-swarm-beta { + margin: 0 0 0.75em; + padding: 0.5em 0.75em; + border: 1px solid var(--yellow); + border-left-width: 4px; + border-radius: 0.4em; + background: color-mix(in srgb, var(--yellow) 12%, transparent); +} diff --git a/frontend/packages/swarm-ui/src/pages/swarm/SwarmPage.tsx b/frontend/packages/swarm-ui/src/pages/swarm/SwarmPage.tsx new file mode 100644 index 00000000..246e1bd9 --- /dev/null +++ b/frontend/packages/swarm-ui/src/pages/swarm/SwarmPage.tsx @@ -0,0 +1,40 @@ +// — what belongs to the swarm as a whole rather than to one agent: +// the hive roster and the notification router. Each is a card that starts +// folded and reads nothing until it is opened. +// +// The notification router is marked beta: its card says so beside the title and +// carries a warning banner inside. +import { Badge } from "@hive/shared/badge.js"; +import { LazyDisclosure } from "../../ui/disclosure/LazyDisclosure.js"; +import { NotificationRouter } from "../notifications/NotificationsPage.js"; +import { HivesCard } from "./HivesCard.js"; +import "./SwarmPage.css"; + +export function SwarmPage() { + return ( + <> +
+ +
+
+ + forge events routed to + agents, as flows + + } + > +

+ beta: the notification router is still changing. + Flows, their wording and what they match may change, and a flow you + wrote may need rewriting after an update. +

+ +
+
+ + ); +} diff --git a/frontend/packages/swarm-ui/src/shell/Shell.tsx b/frontend/packages/swarm-ui/src/shell/Shell.tsx index b3dc6038..6466cdc3 100644 --- a/frontend/packages/swarm-ui/src/shell/Shell.tsx +++ b/frontend/packages/swarm-ui/src/shell/Shell.tsx @@ -57,10 +57,10 @@ const SOLID_KEY = "swarm-ui:solid-backgrounds"; const NAV_ITEMS: { href: string; label: string; accent: string }[] = [ { href: "/agents", label: "agents", accent: "var(--green)" }, + { href: "/swarm", label: "swarm", accent: "var(--yellow)" }, { href: "/reports", label: "reports", accent: "var(--red)" }, { href: "/jobs", label: "jobs", accent: "var(--pink)" }, { href: "/messages", label: "messages", accent: "var(--purple)" }, - { href: "/notifications", label: "notifications", accent: "var(--yellow)" }, { href: "/components", label: "components", accent: "var(--blue)" }, ]; diff --git a/frontend/packages/swarm-ui/src/shell/routes.ts b/frontend/packages/swarm-ui/src/shell/routes.ts index 07238feb..36534860 100644 --- a/frontend/packages/swarm-ui/src/shell/routes.ts +++ b/frontend/packages/swarm-ui/src/shell/routes.ts @@ -38,6 +38,7 @@ export const MOVED_ROUTES: Readonly> = { "/builds": "/jobs", "/review": "/jobs", "/schedules": "/messages", + "/notifications": "/swarm", }; /** Where an old address goes, keeping its query; `null` for any other path. */ diff --git a/frontend/packages/swarm-ui/test/routes.test.mjs b/frontend/packages/swarm-ui/test/routes.test.mjs index f33fa1a4..4add09fa 100644 --- a/frontend/packages/swarm-ui/test/routes.test.mjs +++ b/frontend/packages/swarm-ui/test/routes.test.mjs @@ -69,3 +69,8 @@ test("no old address leads to another old address", () => { assert.equal(to in MOVED_ROUTES, false, to); } }); + +test("the old notifications list goes to the swarm tab, the editor pages stay", () => { + assert.equal(movedHref("/notifications", ""), "/swarm"); + assert.equal(movedHref("/notifications/some-flow", ""), null); +});