Compare commits
13 changed files with 74 additions and 150 deletions
|
|
@ -26,10 +26,11 @@ 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/<name>/terminal` | one agent's live terminal |
|
||||
| `/agents/<name>/subagents/<subagent>/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** |
|
||||
| `/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 |
|
||||
| `/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 |
|
||||
| `/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).
|
||||
Pages that refresh on a timer share one polling rule: the page sends no new
|
||||
|
|
@ -457,9 +458,7 @@ live rows only.
|
|||
|
||||
### Issue report
|
||||
|
||||
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
|
||||
The **issues** page lists the open issues of every repo that has one, from
|
||||
`GET /api/issue-report`, or of one repo from
|
||||
`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
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ swarm's VictoriaLogs; read them in Grafana's **Explore** — see
|
|||
|
||||
**Set up a schedule.** Scheduled prompts are swarm-level: set them with
|
||||
`swarmctl schedule` ([`swarm/schedules.md`](../swarm/schedules.md)) or
|
||||
in the schedules card on the swarm UI's messages tab ([`swarm/ui.md`](../swarm/ui.md)).
|
||||
on the swarm UI's schedules page ([`swarm/ui.md`](../swarm/ui.md)).
|
||||
|
||||
## More depth
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,27 @@
|
|||
// Root shell component — routing only. Each route's content is its own
|
||||
// page component under `./pages/`; this file just maps paths to them.
|
||||
import { Redirect, Route, Router, Switch, useSearch } from "wouter-preact";
|
||||
import { MOVED_ROUTES, movedHref } from "./shell/routes.js";
|
||||
import { jobsHref } from "./pages/builds/builds-types.js";
|
||||
import { Shell } from "./shell/Shell.js";
|
||||
import { useGuardedLocation } from "./shell/useGuardedLocation.js";
|
||||
import { AgentsPage } from "./pages/agents/AgentsPage.js";
|
||||
import { AgentTerminalPage } from "./pages/agents/AgentTerminalPage.js";
|
||||
import { AlertsPage } from "./pages/AlertsPage.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 { ComponentsPage } from "./pages/ComponentsPage.js";
|
||||
import { JobsPage } from "./pages/JobsPage.js";
|
||||
import { ReportsPage } from "./pages/reports/ReportsPage.js";
|
||||
import { IssueReportPage } from "./pages/IssueReportPage.js";
|
||||
import { StartPage } from "./pages/StartPage.js";
|
||||
import { Panel } from "./ui/panel/Panel.js";
|
||||
|
||||
// An address from before the tabs were folded together: replace it with the
|
||||
// tab that holds the page now, query kept.
|
||||
function MovedTo({ from }: { from: string }) {
|
||||
return <Redirect to={movedHref(from, useSearch()) ?? "/"} replace />;
|
||||
// The builds page moved onto the jobs tab. Links made before that, such as the
|
||||
// Details link on a swarm-builder commit status, land here and go on to the
|
||||
// same build's card, query kept.
|
||||
function BuildsMoved() {
|
||||
return <Redirect to={jobsHref(useSearch())} replace />;
|
||||
}
|
||||
|
||||
function NotFound() {
|
||||
|
|
@ -36,12 +38,7 @@ export function App() {
|
|||
<Shell>
|
||||
<Switch>
|
||||
<Route path="/" component={StartPage} />
|
||||
<Route path="/reports" component={ReportsPage} />
|
||||
{Object.keys(MOVED_ROUTES).map((from) => (
|
||||
<Route key={from} path={from}>
|
||||
<MovedTo from={from} />
|
||||
</Route>
|
||||
))}
|
||||
<Route path="/alerts" component={AlertsPage} />
|
||||
<Route path="/agents" component={AgentsPage} />
|
||||
<Route path="/agents/:name/terminal" component={AgentTerminalPage} />
|
||||
<Route
|
||||
|
|
@ -49,12 +46,20 @@ export function App() {
|
|||
component={AgentTerminalPage}
|
||||
/>
|
||||
<Route path="/jobs" component={JobsPage} />
|
||||
<Route path="/schedules">
|
||||
<Redirect to="/messages" replace />
|
||||
</Route>
|
||||
<Route path="/builds" component={BuildsMoved} />
|
||||
<Route path="/messages" component={MessagesPage} />
|
||||
<Route path="/notifications" component={NotificationsPage} />
|
||||
<Route path={NEW_FLOW_PATH}>
|
||||
<FlowEditorPage params={{ flowId: "" }} isNew />
|
||||
</Route>
|
||||
<Route path="/notifications/:flowId" component={FlowEditorPage} />
|
||||
<Route path="/issues" component={IssueReportPage} />
|
||||
<Route path="/review">
|
||||
<Redirect to="/jobs" replace />
|
||||
</Route>
|
||||
<Route path="/components" component={ComponentsPage} />
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// <AlertsCard> — a card of the reports tab: the alerts the swarm's Grafana holds
|
||||
// <AlertsPage> — `/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 `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 { Badge, type BadgeTone } from "@hive/shared/badge.js";
|
||||
import { Button } from "../ui/button/Button.js";
|
||||
import { LazyDisclosure } from "../ui/disclosure/LazyDisclosure.js";
|
||||
import { Panel } from "../ui/panel/Panel.js";
|
||||
import { RelativeTime } from "../ui/relative-time/RelativeTime.js";
|
||||
import {
|
||||
RefreshIntervalPicker,
|
||||
|
|
@ -71,7 +71,7 @@ const COLUMNS: TableColumn<Alert>[] = [
|
|||
|
||||
const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000;
|
||||
|
||||
function AlertsBody() {
|
||||
export function AlertsPage() {
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
const [intervalMs, setIntervalMs] =
|
||||
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
|
||||
|
|
@ -82,22 +82,31 @@ function AlertsBody() {
|
|||
const hidden = alerts && listed ? alerts.length - listed.length : 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="ui-alerts-bar">
|
||||
<a href={GRAFANA_ALERTS_URL} target="_blank" rel="noopener noreferrer">
|
||||
Grafana's alert list
|
||||
</a>
|
||||
{showAll || hidden > 0 ? (
|
||||
<Button onClick={() => setShowAll(!showAll)}>
|
||||
{showAll ? "only firing" : `show ${hidden} more`}
|
||||
</Button>
|
||||
) : null}
|
||||
<RefreshIntervalPicker
|
||||
id="alerts-refresh"
|
||||
value={intervalMs}
|
||||
onChange={setIntervalMs}
|
||||
/>
|
||||
</div>
|
||||
<Panel
|
||||
title="alerts"
|
||||
icon="🚨"
|
||||
actions={
|
||||
<>
|
||||
<a
|
||||
href={GRAFANA_ALERTS_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Grafana's alert list
|
||||
</a>
|
||||
{showAll || hidden > 0 ? (
|
||||
<Button onClick={() => setShowAll(!showAll)}>
|
||||
{showAll ? "only firing" : `show ${hidden} more`}
|
||||
</Button>
|
||||
) : null}
|
||||
<RefreshIntervalPicker
|
||||
id="alerts-refresh"
|
||||
value={intervalMs}
|
||||
onChange={setIntervalMs}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{error ? (
|
||||
<ApiErrorPanel
|
||||
context="failed to load Grafana's alerts"
|
||||
|
|
@ -116,19 +125,6 @@ function AlertsBody() {
|
|||
storageKey="swarm-ui:alerts:table-filters"
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// 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>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* <IssueReportCard> — the repo-picker + hide-blocked toggle sit in one
|
||||
/* <IssueReportPage> — the repo-picker + hide-blocked toggle sit in one
|
||||
row (mirrors CreateAgentForm's row/wrap pattern: flex-wrap so narrow
|
||||
viewports stack instead of overflowing). The label filter used to
|
||||
have its own chip row here; folded into the "labels" column's own
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// <IssueReportCard> — a card of the reports tab: mara's custom issue report: browse open issues
|
||||
// <IssueReportPage> — mara's custom issue report: browse open issues
|
||||
// across every repo that has one, sortable/filterable her own way, with
|
||||
// "hide blocked (open dependency)" as the headline filter and a rank
|
||||
// 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 { Badge } from "@hive/shared/badge.js";
|
||||
import { useLocalSetting } from "@hive/shared/settings-storage.js";
|
||||
import { LazyDisclosure } from "../ui/disclosure/LazyDisclosure.js";
|
||||
import { Panel } from "../ui/panel/Panel.js";
|
||||
import { CheckboxField } from "../ui/checkbox-field/CheckboxField.js";
|
||||
import { SelectField } from "../ui/select-field/SelectField.js";
|
||||
import { Table, type TableColumn } from "../ui/table/Table.js";
|
||||
|
|
@ -128,7 +128,7 @@ function SortHeader({
|
|||
);
|
||||
}
|
||||
|
||||
function IssueReportBody() {
|
||||
export function IssueReportPage() {
|
||||
const [repos, setRepos] = useState<string[] | null>(null);
|
||||
const [repoFilter, setRepoFilter] = useLocalSetting(
|
||||
REPO_FILTER_KEY,
|
||||
|
|
@ -370,7 +370,7 @@ function IssueReportBody() {
|
|||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Panel title="issue report" icon="📋">
|
||||
<div class="issue-report-controls">
|
||||
<SelectField
|
||||
id="issue-report-repo"
|
||||
|
|
@ -408,20 +408,6 @@ function IssueReportBody() {
|
|||
storageKey={TABLE_FILTERS_KEY}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// 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>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,11 @@ export const OUTCOME_TONE: Record<BuildOutcome, BadgeTone> = {
|
|||
error: "warning",
|
||||
};
|
||||
|
||||
/** Where an old `/builds` link goes: the jobs page, keeping its query. */
|
||||
export function jobsHref(search: string): string {
|
||||
return search === "" ? "/jobs" : `/jobs?${search}`;
|
||||
}
|
||||
|
||||
/** The run a `?run=<id>` query names, or `null` when it names none. */
|
||||
export function wantedRun(search: string): string | null {
|
||||
const id = new URLSearchParams(search).get("run");
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
/* 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;
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
// <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>
|
||||
);
|
||||
}
|
||||
|
|
@ -51,10 +51,11 @@ const MOTION_KEY = "swarm-ui:motion-override";
|
|||
|
||||
const NAV_ITEMS: { href: string; label: string; accent: string }[] = [
|
||||
{ href: "/agents", label: "agents", accent: "var(--green)" },
|
||||
{ href: "/reports", label: "reports", accent: "var(--red)" },
|
||||
{ href: "/alerts", label: "alerts", 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: "/issues", label: "issues", accent: "var(--yellow)" },
|
||||
{ href: "/components", label: "components", accent: "var(--blue)" },
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -27,22 +27,3 @@ export function visibleNavItems<T extends { href: string }>(
|
|||
if (!hideDeveloper) return items;
|
||||
return items.filter((item) => !DEVELOPER_ROUTES.includes(item.href));
|
||||
}
|
||||
|
||||
// Tabs that were folded into another one. Each old address redirects to its
|
||||
// new tab, so a bookmark or a link made before the move still lands. The
|
||||
// builds page's `?run=` query rides along, which a swarm-builder commit
|
||||
// status link relies on.
|
||||
export const MOVED_ROUTES: Readonly<Record<string, string>> = {
|
||||
"/alerts": "/reports",
|
||||
"/issues": "/reports",
|
||||
"/builds": "/jobs",
|
||||
"/review": "/jobs",
|
||||
"/schedules": "/messages",
|
||||
};
|
||||
|
||||
/** Where an old address goes, keeping its query; `null` for any other path. */
|
||||
export function movedHref(path: string, search: string): string | null {
|
||||
const to = MOVED_ROUTES[path];
|
||||
if (to === undefined) return null;
|
||||
return search === "" ? to : `${to}?${search}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@
|
|||
// stripping, so the `.ts` module is imported as it is.
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { bodyText, wantedRun } from "../src/pages/builds/builds-types.ts";
|
||||
import {
|
||||
bodyText,
|
||||
jobsHref,
|
||||
wantedRun,
|
||||
} from "../src/pages/builds/builds-types.ts";
|
||||
|
||||
const base = {
|
||||
id: "atlas-abc-1",
|
||||
|
|
@ -49,3 +53,8 @@ test("a run with no text for its outcome shows no body", () => {
|
|||
assert.equal(bodyText({ ...base, outcome: "failed" }), null);
|
||||
assert.equal(bodyText({ ...base, outcome: "built", error_tail: "x" }), null);
|
||||
});
|
||||
|
||||
test("an old builds link goes to the jobs page and keeps its query", () => {
|
||||
assert.equal(jobsHref("run=atlas-abc-1"), "/jobs?run=atlas-abc-1");
|
||||
assert.equal(jobsHref(""), "/jobs");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +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 {
|
||||
MOVED_ROUTES,
|
||||
isFullBleedRoute,
|
||||
movedHref,
|
||||
visibleNavItems,
|
||||
} from "../src/shell/routes.ts";
|
||||
import { isFullBleedRoute, visibleNavItems } from "../src/shell/routes.ts";
|
||||
|
||||
test("the start page is full-bleed", () => {
|
||||
assert.equal(isFullBleedRoute("/"), true);
|
||||
|
|
@ -47,25 +42,3 @@ test("hide developer details takes the components tab out of the nav", () => {
|
|||
test("with it off the nav is whole again", () => {
|
||||
assert.deepEqual(visibleNavItems(NAV, false), NAV);
|
||||
});
|
||||
|
||||
test("an old address goes to the tab that holds its page, query kept", () => {
|
||||
assert.equal(movedHref("/alerts", ""), "/reports");
|
||||
assert.equal(movedHref("/issues", ""), "/reports");
|
||||
assert.equal(movedHref("/review", ""), "/jobs");
|
||||
assert.equal(movedHref("/schedules", ""), "/messages");
|
||||
assert.equal(
|
||||
movedHref("/builds", "run=atlas-abc-1"),
|
||||
"/jobs?run=atlas-abc-1",
|
||||
);
|
||||
});
|
||||
|
||||
test("a current tab is not moved", () => {
|
||||
assert.equal(movedHref("/jobs", ""), null);
|
||||
assert.equal(movedHref("/agents", "x=1"), null);
|
||||
});
|
||||
|
||||
test("no old address leads to another old address", () => {
|
||||
for (const to of Object.values(MOVED_ROUTES)) {
|
||||
assert.equal(to in MOVED_ROUTES, false, to);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue