Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: show only firing alerts on the alerts page and link to Grafana's list

The page listed every non-normal instance. It now keeps the ones in the
Alerting state and links to Grafana's alert-groups view for the pending,
no-data and error ones.
This commit is contained in:
iris 2026-10-18 13:07:23 +02:00 • committed by mara
commit 842b37dc33
2 changed files with 33 additions and 24 deletions

View file

@ -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` | 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/<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 (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 | | `/jobs` | the controller's job graph — where agent creation and credential mints show progress |
| `/schedules` | the scheduled prompts: add, edit, pause, resume and remove | | `/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 | | `/broadcast` | send one message to several agents at once, picked from a grid of their icons |

View file

@ -1,10 +1,12 @@
// <AlertsPage> — `/alerts`' content: the alerts the swarm's Grafana holds // <AlertsPage> — `/alerts`' content: the alerts of the swarm's Grafana that
// now, from swarm-controller's `GET /api/alerts`. Same shape as `HivesPage`: // are firing now, from swarm-controller's `GET /api/alerts` (which also lists
// one fetch on a `RefreshIntervalPicker` cadence, rendered as a `Table`. // 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 { useState } from "preact/hooks";
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js"; 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, type BadgeTone } from "@hive/shared/badge.js"; import { Badge } from "@hive/shared/badge.js";
import { Panel } from "../ui/panel/Panel.js"; import { Panel } from "../ui/panel/Panel.js";
import { RelativeTime } from "../ui/relative-time/RelativeTime.js"; import { RelativeTime } from "../ui/relative-time/RelativeTime.js";
import { import {
@ -17,19 +19,18 @@ import { Table, type TableColumn } from "../ui/table/Table.js";
interface Alert { interface Alert {
name: string; name: string;
// Grafana's state for the instance: `Alerting`, `Pending`, `NoData` or // Grafana's state for the instance: `Alerting`, `Pending`, `NoData` or
// `Error`. Anything else renders neutral. // `Error`.
state: string; state: string;
active_at: string | null; active_at: string | null;
summary: string | null; summary: string | null;
labels: Record<string, string>; labels: Record<string, string>;
} }
const STATE_TONE: Record<string, BadgeTone> = { // Grafana's alert-groups view, where the instances this page leaves out are.
Alerting: "negative", const GRAFANA_ALERTS_URL =
Error: "negative", "https://grafana.constellation.darkest.space/alerting/groups";
NoData: "warning",
Pending: "warning", const FIRING = "Alerting";
};
// Grafana reports an instance that never became active with the zero time, // Grafana reports an instance that never became active with the zero time,
// which `Date.parse` reads as year 1. // which `Date.parse` reads as year 1.
@ -59,7 +60,7 @@ const COLUMNS: TableColumn<Alert>[] = [
const since = activeSinceMs(a); const since = activeSinceMs(a);
return ( return (
<Badge <Badge
tone={STATE_TONE[a.state] ?? "neutral"} tone="negative"
value={ value={
since !== null ? ( since !== null ? (
<> <>
@ -72,9 +73,7 @@ const COLUMNS: TableColumn<Alert>[] = [
/> />
); );
}, },
sortBy: (a) => a.state, sortBy: (a) => activeSinceMs(a) ?? 0,
filterValues: (a) => [a.state],
filterMode: "multiselect",
}, },
{ {
key: "summary", key: "summary",
@ -90,6 +89,7 @@ const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000;
export function AlertsPage() { export function AlertsPage() {
const [alerts, setAlerts] = useState<Alert[] | null>(null); const [alerts, setAlerts] = useState<Alert[] | null>(null);
const [error, setError] = useState<ProblemDetails | null>(null); const [error, setError] = useState<ProblemDetails | null>(null);
const firing = alerts?.filter((a) => a.state === FIRING) ?? null;
const [intervalMs, setIntervalMs] = const [intervalMs, setIntervalMs] =
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS); useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
@ -110,11 +110,20 @@ export function AlertsPage() {
title="alerts" title="alerts"
icon="🚨" icon="🚨"
actions={ actions={
<RefreshIntervalPicker <>
id="alerts-refresh" <a
value={intervalMs} href={GRAFANA_ALERTS_URL}
onChange={setIntervalMs} target="_blank"
/> rel="noopener noreferrer"
>
all alerts in Grafana
</a>
<RefreshIntervalPicker
id="alerts-refresh"
value={intervalMs}
onChange={setIntervalMs}
/>
</>
} }
> >
{error ? ( {error ? (
@ -124,11 +133,11 @@ export function AlertsPage() {
/> />
) : null} ) : null}
{!error && alerts === null ? <p>loading…</p> : null} {!error && alerts === null ? <p>loading…</p> : null}
{alerts && alerts.length === 0 ? <p>no alerts.</p> : null} {firing && firing.length === 0 ? <p>nothing is alerting.</p> : null}
{alerts && alerts.length > 0 ? ( {firing && firing.length > 0 ? (
<Table <Table
columns={COLUMNS} columns={COLUMNS}
rows={alerts} rows={firing}
rowKey={alertKey} rowKey={alertKey}
storageKey="swarm-ui:alerts:table-filters" storageKey="swarm-ui:alerts:table-filters"
/> />