Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: alerts and the issue report are folded cards on a reports tab

The alerts and issues tabs are one reports tab in the nav. Each page, unchanged
inside, is a card that starts shut and reads nothing until opened; the alerts
header's link, button and refresh picker move into the card's body. /alerts and
/issues redirect to /reports. Docs follow.
This commit is contained in:
iris 2026-10-26 21:16:05 +01:00 • committed by mara
commit 599747bacb
7 changed files with 96 additions and 43 deletions

View file

@ -26,11 +26,10 @@ shut. A dropdown inside a dialog opens above the dialog and the dialog doesn't c
| `/agents` | every agent, under a folded **hives** card with the hive directory and each hive's last reported status: status, config PR, wanted state, inference settings, resource caps, permissions; create agents (with their permissions, optionally from an [archetype](archetypes.md)), link forge, matrix and GitHub accounts | | `/agents` | every agent, under a folded **hives** card with the hive directory and each hive's last reported status: status, config PR, wanted state, inference settings, resource caps, permissions; create agents (with their permissions, optionally from an [archetype](archetypes.md)), link forge, matrix and GitHub accounts |
| `/agents/<name>/terminal` | one agent's live terminal | | `/agents/<name>/terminal` | one agent's live terminal |
| `/agents/<name>/subagents/<subagent>/terminal` | one of that agent's subagents' live terminal | | `/agents/<name>/subagents/<subagent>/terminal` | one of that agent's subagents' live terminal |
| `/alerts` | 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 | | `/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 of agent config PRs, with what nix printed when one failed, and the review classifier's recent runs, a try-out on a real PR, and its settings | | `/jobs` | the controller's job graph, where agent creation and credential mints show progress, and under it the swarm builder's recent builds of agent config PRs, 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 | | `/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 | | `/notifications` | the notification router's flows: a switch for each built-in, every flow drawn as a graph, and an editor for custom flows |
| `/issues` | a cross-repo issue report |
Everything it shows comes from [`swarm-controller`](../../swarm-controller/README.md). 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 Pages that refresh on a timer share one polling rule: the page sends no new
@ -458,7 +457,9 @@ live rows only.
### Issue report ### Issue report
The **issues** page lists the open issues of every repo that has one, from The card starts shut. The old `/issues` address redirects to the reports tab, as `/alerts` does.
The **issue report** card on the **reports** tab lists the open issues of every repo that has one, from
`GET /api/issue-report`, or of one repo from `GET /api/issue-report`, or of one repo from
`GET /api/repos/{org}/{repo}/issue-report`. Each row shows whether an open `GET /api/repos/{org}/{repo}/issue-report`. Each row shows whether an open
dependency blocks the issue and how many other issues depend on it. The dependency blocks the issue and how many other issues depend on it. The

View file

@ -6,14 +6,13 @@ import { Shell } from "./shell/Shell.js";
import { useGuardedLocation } from "./shell/useGuardedLocation.js"; import { useGuardedLocation } from "./shell/useGuardedLocation.js";
import { AgentsPage } from "./pages/agents/AgentsPage.js"; import { AgentsPage } from "./pages/agents/AgentsPage.js";
import { AgentTerminalPage } from "./pages/agents/AgentTerminalPage.js"; import { AgentTerminalPage } from "./pages/agents/AgentTerminalPage.js";
import { AlertsPage } from "./pages/AlertsPage.js";
import { MessagesPage } from "./pages/messages/MessagesPage.js"; import { MessagesPage } from "./pages/messages/MessagesPage.js";
import { NEW_FLOW_PATH } from "./pages/notifications/FlowDraft.js"; import { NEW_FLOW_PATH } from "./pages/notifications/FlowDraft.js";
import { FlowEditorPage } from "./pages/notifications/FlowEditorPage.js"; import { FlowEditorPage } from "./pages/notifications/FlowEditorPage.js";
import { NotificationsPage } from "./pages/notifications/NotificationsPage.js"; import { NotificationsPage } from "./pages/notifications/NotificationsPage.js";
import { ComponentsPage } from "./pages/ComponentsPage.js"; import { ComponentsPage } from "./pages/ComponentsPage.js";
import { JobsPage } from "./pages/JobsPage.js"; import { JobsPage } from "./pages/JobsPage.js";
import { IssueReportPage } from "./pages/IssueReportPage.js"; import { ReportsPage } from "./pages/reports/ReportsPage.js";
import { StartPage } from "./pages/StartPage.js"; import { StartPage } from "./pages/StartPage.js";
import { Panel } from "./ui/panel/Panel.js"; import { Panel } from "./ui/panel/Panel.js";
@ -38,7 +37,10 @@ export function App() {
<Shell> <Shell>
<Switch> <Switch>
<Route path="/" component={StartPage} /> <Route path="/" component={StartPage} />
<Route path="/alerts" component={AlertsPage} /> <Route path="/reports" component={ReportsPage} />
<Route path="/alerts">
<Redirect to="/reports" replace />
</Route>
<Route path="/agents" component={AgentsPage} /> <Route path="/agents" component={AgentsPage} />
<Route path="/agents/:name/terminal" component={AgentTerminalPage} /> <Route path="/agents/:name/terminal" component={AgentTerminalPage} />
<Route <Route
@ -56,7 +58,9 @@ export function App() {
<FlowEditorPage params={{ flowId: "" }} isNew /> <FlowEditorPage params={{ flowId: "" }} isNew />
</Route> </Route>
<Route path="/notifications/:flowId" component={FlowEditorPage} /> <Route path="/notifications/:flowId" component={FlowEditorPage} />
<Route path="/issues" component={IssueReportPage} /> <Route path="/issues">
<Redirect to="/reports" replace />
</Route>
<Route path="/review"> <Route path="/review">
<Redirect to="/jobs" replace /> <Redirect to="/jobs" replace />
</Route> </Route>

View file

@ -1,4 +1,4 @@
// <AlertsPage> — `/alerts`' content: the alerts the swarm's Grafana holds // <AlertsCard> — a card of the reports tab: the alerts the swarm's Grafana holds
// now, from swarm-controller's `GET /api/alerts`. Only the firing ones show at // 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. // first; a button in the header adds the pending, no-data and errored ones.
// Same shape as `HivesCard`: one fetch on a `RefreshIntervalPicker` cadence, // Same shape as `HivesCard`: one fetch on a `RefreshIntervalPicker` cadence,
@ -7,7 +7,7 @@ import { useState } from "preact/hooks";
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js"; import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
import { Badge, type BadgeTone } from "@hive/shared/badge.js"; import { Badge, type BadgeTone } from "@hive/shared/badge.js";
import { Button } from "../ui/button/Button.js"; import { Button } from "../ui/button/Button.js";
import { Panel } from "../ui/panel/Panel.js"; import { LazyDisclosure } from "../ui/disclosure/LazyDisclosure.js";
import { RelativeTime } from "../ui/relative-time/RelativeTime.js"; import { RelativeTime } from "../ui/relative-time/RelativeTime.js";
import { import {
RefreshIntervalPicker, RefreshIntervalPicker,
@ -71,7 +71,7 @@ const COLUMNS: TableColumn<Alert>[] = [
const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000; const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000;
export function AlertsPage() { function AlertsBody() {
const [showAll, setShowAll] = useState(false); const [showAll, setShowAll] = useState(false);
const [intervalMs, setIntervalMs] = const [intervalMs, setIntervalMs] =
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS); useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
@ -82,16 +82,9 @@ export function AlertsPage() {
const hidden = alerts && listed ? alerts.length - listed.length : 0; const hidden = alerts && listed ? alerts.length - listed.length : 0;
return ( return (
<Panel
title="alerts"
icon="🚨"
actions={
<> <>
<a <div class="ui-alerts-bar">
href={GRAFANA_ALERTS_URL} <a href={GRAFANA_ALERTS_URL} target="_blank" rel="noopener noreferrer">
target="_blank"
rel="noopener noreferrer"
>
Grafana's alert list Grafana's alert list
</a> </a>
{showAll || hidden > 0 ? ( {showAll || hidden > 0 ? (
@ -104,9 +97,7 @@ export function AlertsPage() {
value={intervalMs} value={intervalMs}
onChange={setIntervalMs} onChange={setIntervalMs}
/> />
</> </div>
}
>
{error ? ( {error ? (
<ApiErrorPanel <ApiErrorPanel
context="failed to load Grafana's alerts" context="failed to load Grafana's alerts"
@ -125,6 +116,19 @@ export function AlertsPage() {
storageKey="swarm-ui:alerts:table-filters" storageKey="swarm-ui:alerts:table-filters"
/> />
) : null} ) : null}
</Panel> </>
);
}
// A folded card on the reports tab: read and polled only once it is opened.
export function AlertsCard() {
return (
<LazyDisclosure
title="alerts"
icon="🚨"
hint="what the swarm's Grafana holds now"
>
<AlertsBody />
</LazyDisclosure>
); );
} }

View file

@ -1,4 +1,4 @@
// <IssueReportPage> — mara's custom issue report: browse open issues // <IssueReportCard> — a card of the reports tab: mara's custom issue report: browse open issues
// across every repo that has one, sortable/filterable her own way, with // across every repo that has one, sortable/filterable her own way, with
// "hide blocked (open dependency)" as the headline filter and a rank // "hide blocked (open dependency)" as the headline filter and a rank
// column for how many other open issues depend on each one. Backed by // column for how many other open issues depend on each one. Backed by
@ -32,7 +32,7 @@ import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js"; import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
import { Badge } from "@hive/shared/badge.js"; import { Badge } from "@hive/shared/badge.js";
import { useLocalSetting } from "@hive/shared/settings-storage.js"; import { useLocalSetting } from "@hive/shared/settings-storage.js";
import { Panel } from "../ui/panel/Panel.js"; import { LazyDisclosure } from "../ui/disclosure/LazyDisclosure.js";
import { CheckboxField } from "../ui/checkbox-field/CheckboxField.js"; import { CheckboxField } from "../ui/checkbox-field/CheckboxField.js";
import { SelectField } from "../ui/select-field/SelectField.js"; import { SelectField } from "../ui/select-field/SelectField.js";
import { Table, type TableColumn } from "../ui/table/Table.js"; import { Table, type TableColumn } from "../ui/table/Table.js";
@ -128,7 +128,7 @@ function SortHeader({
); );
} }
export function IssueReportPage() { function IssueReportBody() {
const [repos, setRepos] = useState<string[] | null>(null); const [repos, setRepos] = useState<string[] | null>(null);
const [repoFilter, setRepoFilter] = useLocalSetting( const [repoFilter, setRepoFilter] = useLocalSetting(
REPO_FILTER_KEY, REPO_FILTER_KEY,
@ -370,7 +370,7 @@ export function IssueReportPage() {
]; ];
return ( return (
<Panel title="issue report" icon="📋"> <>
<div class="issue-report-controls"> <div class="issue-report-controls">
<SelectField <SelectField
id="issue-report-repo" id="issue-report-repo"
@ -408,6 +408,20 @@ export function IssueReportPage() {
storageKey={TABLE_FILTERS_KEY} storageKey={TABLE_FILTERS_KEY}
/> />
) : null} ) : null}
</Panel> </>
);
}
// A folded card on the reports tab: the repo list and the report are read only
// once it is opened.
export function IssueReportCard() {
return (
<LazyDisclosure
title="issue report"
icon="📋"
hint="open issues across every repo, ranked by what they block"
>
<IssueReportBody />
</LazyDisclosure>
); );
} }

View file

@ -0,0 +1,16 @@
/* The reports tab: the folded cards, one gap apart. */
.ui-reports-page {
display: flex;
flex-direction: column;
gap: 1em;
}
/* The bar at the top of the alerts card: the Grafana link, the show-more
button and the refresh picker, to the right. */
.ui-alerts-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 0.5em 0.75em;
margin-block-end: 0.5em;
}

View file

@ -0,0 +1,15 @@
// <ReportsPage> — `/reports`: what the swarm reports about itself, as two
// folded cards: the alerts Grafana holds now, and mara's issue report. Neither
// reads anything until it is opened.
import { AlertsCard } from "../AlertsPage.js";
import { IssueReportCard } from "../IssueReportPage.js";
import "./ReportsPage.css";
export function ReportsPage() {
return (
<div class="ui-reports-page">
<AlertsCard />
<IssueReportCard />
</div>
);
}

View file

@ -51,11 +51,10 @@ const MOTION_KEY = "swarm-ui:motion-override";
const NAV_ITEMS: { href: string; label: string; accent: string }[] = [ const NAV_ITEMS: { href: string; label: string; accent: string }[] = [
{ href: "/agents", label: "agents", accent: "var(--green)" }, { href: "/agents", label: "agents", accent: "var(--green)" },
{ href: "/alerts", label: "alerts", accent: "var(--red)" }, { href: "/reports", label: "reports", accent: "var(--red)" },
{ href: "/jobs", label: "jobs", accent: "var(--pink)" }, { href: "/jobs", label: "jobs", accent: "var(--pink)" },
{ href: "/messages", label: "messages", accent: "var(--purple)" }, { href: "/messages", label: "messages", accent: "var(--purple)" },
{ href: "/notifications", label: "notifications", accent: "var(--yellow)" }, { href: "/notifications", label: "notifications", accent: "var(--yellow)" },
{ href: "/issues", label: "issues", accent: "var(--yellow)" },
{ href: "/components", label: "components", accent: "var(--blue)" }, { href: "/components", label: "components", accent: "var(--blue)" },
]; ];