diff --git a/docs/swarm/ui.md b/docs/swarm/ui.md index b6c4e42b..566a5f6b 100644 --- a/docs/swarm/ui.md +++ b/docs/swarm/ui.md @@ -14,7 +14,7 @@ domain, covers host-level detail for one hive. | `/agents` | every agent: status, config PR, wanted state, inference settings, resource caps; create agents (with their permissions, optionally from an [archetype](archetypes.md)), link forge, matrix and GitHub accounts | | `/agents//terminal` | one agent's live terminal | | `/agents//subagents//terminal` | one of that agent's subagents' live terminal | -| `/alerts` | the alerts the swarm's Grafana holds now (firing, pending, no data, error), read through the controller with a Viewer service-account token | +| `/alerts` | the alerts of the swarm's Grafana that are firing now, with a link to Grafana's full alert list for the pending, no-data and error ones; read through the controller with a Viewer service-account token | | `/jobs` | the controller's job graph — where agent creation and credential mints show progress | | `/schedules` | the scheduled prompts: add, edit, pause, resume and remove | | `/broadcast` | send one message to several agents at once, picked from a grid of their icons | diff --git a/frontend/packages/swarm-ui/src/pages/AlertsPage.tsx b/frontend/packages/swarm-ui/src/pages/AlertsPage.tsx index b13d2ef7..1cb90f9b 100644 --- a/frontend/packages/swarm-ui/src/pages/AlertsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/AlertsPage.tsx @@ -1,10 +1,12 @@ -// — `/alerts`' content: the alerts the swarm's Grafana holds -// now, from swarm-controller's `GET /api/alerts`. Same shape as `HivesPage`: -// one fetch on a `RefreshIntervalPicker` cadence, rendered as a `Table`. +// — `/alerts`' content: the alerts of the swarm's Grafana that +// are firing now, from swarm-controller's `GET /api/alerts` (which also lists +// pending, no-data and errored instances; those stay in Grafana). Same shape +// as `HivesPage`: one fetch on a `RefreshIntervalPicker` cadence, rendered as +// a `Table`. 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 { Badge } from "@hive/shared/badge.js"; import { Panel } from "../ui/panel/Panel.js"; import { RelativeTime } from "../ui/relative-time/RelativeTime.js"; import { @@ -17,19 +19,18 @@ import { Table, type TableColumn } from "../ui/table/Table.js"; interface Alert { name: string; // Grafana's state for the instance: `Alerting`, `Pending`, `NoData` or - // `Error`. Anything else renders neutral. + // `Error`. state: string; active_at: string | null; summary: string | null; labels: Record; } -const STATE_TONE: Record = { - Alerting: "negative", - Error: "negative", - NoData: "warning", - Pending: "warning", -}; +// Grafana's alert-groups view, where the instances this page leaves out are. +const GRAFANA_ALERTS_URL = + "https://grafana.constellation.darkest.space/alerting/groups"; + +const FIRING = "Alerting"; // Grafana reports an instance that never became active with the zero time, // which `Date.parse` reads as year 1. @@ -59,7 +60,7 @@ const COLUMNS: TableColumn[] = [ const since = activeSinceMs(a); return ( @@ -72,9 +73,7 @@ const COLUMNS: TableColumn[] = [ /> ); }, - sortBy: (a) => a.state, - filterValues: (a) => [a.state], - filterMode: "multiselect", + sortBy: (a) => activeSinceMs(a) ?? 0, }, { key: "summary", @@ -90,6 +89,7 @@ const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000; export function AlertsPage() { const [alerts, setAlerts] = useState(null); const [error, setError] = useState(null); + const firing = alerts?.filter((a) => a.state === FIRING) ?? null; const [intervalMs, setIntervalMs] = useState(DEFAULT_INTERVAL_MS); @@ -110,11 +110,20 @@ export function AlertsPage() { title="alerts" icon="🚨" actions={ - + <> + + all alerts in Grafana + + + } > {error ? ( @@ -124,11 +133,11 @@ export function AlertsPage() { /> ) : null} {!error && alerts === null ?

loading…

: null} - {alerts && alerts.length === 0 ?

no alerts.

: null} - {alerts && alerts.length > 0 ? ( + {firing && firing.length === 0 ?

nothing is alerting.

: null} + {firing && firing.length > 0 ? (