swarm-ui: a swarm tab after agents holding the hives card and the beta notification router; /notifications redirects to it
This commit is contained in:
parent
3cc4fdb54c
commit
0302582d1c
12 changed files with 94 additions and 49 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/<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** |
|
||||
| `/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
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
/>
|
||||
<Route path="/jobs" component={JobsPage} />
|
||||
<Route path="/messages" component={MessagesPage} />
|
||||
<Route path="/notifications" component={NotificationsPage} />
|
||||
<Route path="/swarm" component={SwarmPage} />
|
||||
<Route path={NEW_FLOW_PATH}>
|
||||
<FlowEditorPage params={{ flowId: "" }} isNew />
|
||||
</Route>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<>
|
||||
<div class="ui-agents-hives">
|
||||
<HivesCard />
|
||||
</div>
|
||||
{/* 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 ? (
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// <NotificationsPage> — 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.
|
||||
// <NotificationRouter> — 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 (<FlowEditorPage>).
|
||||
|
|
@ -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<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
|
||||
const {
|
||||
|
|
@ -96,22 +96,17 @@ export function NotificationsPage() {
|
|||
);
|
||||
|
||||
return (
|
||||
<Panel
|
||||
title="notifications"
|
||||
icon="🔔"
|
||||
actions={
|
||||
<div class="notify-panel-actions">
|
||||
<RefreshIntervalPicker
|
||||
id="notifications-refresh"
|
||||
value={intervalMs}
|
||||
onChange={setIntervalMs}
|
||||
/>
|
||||
<Button variant="primary" onClick={() => draftFrom()}>
|
||||
create flow
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<>
|
||||
<div class="notify-panel-actions">
|
||||
<RefreshIntervalPicker
|
||||
id="notifications-refresh"
|
||||
value={intervalMs}
|
||||
onChange={setIntervalMs}
|
||||
/>
|
||||
<Button variant="primary" onClick={() => draftFrom()}>
|
||||
create flow
|
||||
</Button>
|
||||
</div>
|
||||
{error ? (
|
||||
<ApiErrorPanel context="failed to load the flows" problem={error} />
|
||||
) : null}
|
||||
|
|
@ -192,6 +187,6 @@ export function NotificationsPage() {
|
|||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</Panel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
162
frontend/packages/swarm-ui/src/pages/swarm/HivesCard.tsx
Normal file
162
frontend/packages/swarm-ui/src/pages/swarm/HivesCard.tsx
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
// <HivesCard> — the hive roster, a card on the agents page that starts folded.
|
||||
// Fetches
|
||||
// swarm-controller's `GET /api/hives/status`, the swarm-wide status
|
||||
// aggregate: one row per roster hive, freshness derived at read time
|
||||
// from the status bucket. Supersedes the earlier `GET /api/hives` +
|
||||
// static "configured" chip placeholder now that the real rollup
|
||||
// exists — same component, richer data, no rebuild, exactly the plan
|
||||
// that placeholder's comment laid out. Its own component/file rather
|
||||
// than living in `App.tsx`, matching `JobsPage`'s shape: `App.tsx` is
|
||||
// routing, a page owns its own fetch + render.
|
||||
//
|
||||
// Polls on a `RefreshIntervalPicker` cadence rather than fetching once
|
||||
// at mount — no inputs on this page, so a re-fetch clobbering an
|
||||
// in-progress edit (the hook's stated caller obligation) isn't a live
|
||||
// concern here.
|
||||
import { useState } from "preact/hooks";
|
||||
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
||||
import { Badge, type BadgeTone } from "@hive/shared/badge.js";
|
||||
import { LazyDisclosure } from "../../ui/disclosure/LazyDisclosure.js";
|
||||
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
||||
import {
|
||||
RefreshIntervalPicker,
|
||||
type RefreshIntervalMs,
|
||||
} from "../../ui/refresh-interval/RefreshInterval.js";
|
||||
import { fetchJson, usePolled } from "../../ui/polled/usePolled.js";
|
||||
import { Table, type TableColumn } from "../../ui/table/Table.js";
|
||||
import { domainCell } from "../hive-domain.js";
|
||||
|
||||
type Freshness = "fresh" | "stale" | "never_reported" | "unknown";
|
||||
|
||||
interface HiveStatus {
|
||||
name: string;
|
||||
domain: string | null;
|
||||
dashboard: string | null;
|
||||
freshness: Freshness;
|
||||
last_seen_unix: number | null;
|
||||
// `age_seconds` (a once-computed-at-fetch age) is part of the wire
|
||||
// shape but deliberately unused here — rendering it directly is the
|
||||
// exact bug `RelativeTime` exists to fix, so this page recomputes the
|
||||
// age client-side from `last_seen_unix` instead.
|
||||
}
|
||||
|
||||
// One tone + label per freshness value. `unknown` (reporting but absent
|
||||
// from the roster) reads `negative` — it's the one case this endpoint
|
||||
// cannot vouch for at all, per `status.rs`'s doc comment.
|
||||
const FRESHNESS: Record<Freshness, { tone: BadgeTone; label: string }> = {
|
||||
fresh: { tone: "positive", label: "fresh" },
|
||||
stale: { tone: "warning", label: "stale" },
|
||||
never_reported: { tone: "neutral", label: "never reported" },
|
||||
unknown: { tone: "negative", label: "unknown" },
|
||||
};
|
||||
|
||||
const COLUMNS: TableColumn<HiveStatus>[] = [
|
||||
{
|
||||
key: "name",
|
||||
header: "name",
|
||||
render: (h) => h.name,
|
||||
sortBy: (h) => h.name,
|
||||
filterValue: (h) => h.name,
|
||||
},
|
||||
{
|
||||
key: "domain",
|
||||
header: "domain",
|
||||
render: (h) => {
|
||||
const { text, href } = domainCell(h);
|
||||
if (!text) return "—";
|
||||
return href ? (
|
||||
<a href={href} target="_blank" rel="noreferrer">
|
||||
{text}
|
||||
</a>
|
||||
) : (
|
||||
text
|
||||
);
|
||||
},
|
||||
sortBy: (h) => domainCell(h).text,
|
||||
filterValue: (h) => domainCell(h).text,
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
header: "status",
|
||||
render: (h) => {
|
||||
const { tone, label } = FRESHNESS[h.freshness];
|
||||
return (
|
||||
<Badge
|
||||
tone={tone}
|
||||
value={
|
||||
h.last_seen_unix !== null ? (
|
||||
<>
|
||||
{label} (<RelativeTime epochMs={h.last_seen_unix * 1000} />)
|
||||
</>
|
||||
) : (
|
||||
label
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
sortBy: (h) => FRESHNESS[h.freshness].label,
|
||||
// Multiselect (not the single-value `"select"` mode) so the operator
|
||||
// can filter for e.g. "stale or offline" in one pass rather than one
|
||||
// freshness state at a time.
|
||||
filterValues: (h) => [FRESHNESS[h.freshness].label],
|
||||
filterMode: "multiselect",
|
||||
},
|
||||
];
|
||||
|
||||
// 30s default: this card has no inputs to interrupt, and the whole
|
||||
// point of a refresh-interval control is "no manual reload needed" — an
|
||||
// operator who wants it off still can, but the out-of-the-box behaviour
|
||||
// should actually solve the staleness problem rather than require an
|
||||
// opt-in every visit.
|
||||
const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000;
|
||||
|
||||
// The roster is read on the first open and followed after, so an agents page
|
||||
// where the card is never opened does not poll it.
|
||||
function HiveRoster() {
|
||||
const [intervalMs, setIntervalMs] =
|
||||
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
|
||||
const { data: hives, error } = usePolled(intervalMs, (signal) =>
|
||||
fetchJson<HiveStatus[]>("/api/hives/status", signal),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="ui-hives-refresh">
|
||||
<RefreshIntervalPicker
|
||||
id="hives-refresh"
|
||||
value={intervalMs}
|
||||
onChange={setIntervalMs}
|
||||
/>
|
||||
</div>
|
||||
{error ? (
|
||||
<ApiErrorPanel
|
||||
context="failed to load the hive roster"
|
||||
problem={error}
|
||||
/>
|
||||
) : null}
|
||||
{!error && hives === null ? <p>loading…</p> : null}
|
||||
{hives ? (
|
||||
<Table
|
||||
columns={COLUMNS}
|
||||
rows={hives}
|
||||
rowKey={(h) => h.name}
|
||||
emptyMessage="no hives in the roster yet"
|
||||
storageKey="swarm-ui:hives:table-filters"
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function HivesCard() {
|
||||
return (
|
||||
<LazyDisclosure
|
||||
title="hives"
|
||||
icon="🐝"
|
||||
hint="the swarm's hives and when each last reported"
|
||||
>
|
||||
<HiveRoster />
|
||||
</LazyDisclosure>
|
||||
);
|
||||
}
|
||||
19
frontend/packages/swarm-ui/src/pages/swarm/SwarmPage.css
Normal file
19
frontend/packages/swarm-ui/src/pages/swarm/SwarmPage.css
Normal file
|
|
@ -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);
|
||||
}
|
||||
40
frontend/packages/swarm-ui/src/pages/swarm/SwarmPage.tsx
Normal file
40
frontend/packages/swarm-ui/src/pages/swarm/SwarmPage.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// <SwarmPage> — 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 (
|
||||
<>
|
||||
<div class="ui-swarm-card">
|
||||
<HivesCard />
|
||||
</div>
|
||||
<div class="ui-swarm-card">
|
||||
<LazyDisclosure
|
||||
title="notification router"
|
||||
icon="🔔"
|
||||
hint={
|
||||
<>
|
||||
<Badge tone="warning" value="beta" /> forge events routed to
|
||||
agents, as flows
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p class="ui-swarm-beta" role="note">
|
||||
<strong>beta:</strong> 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.
|
||||
</p>
|
||||
<NotificationRouter />
|
||||
</LazyDisclosure>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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)" },
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ export const MOVED_ROUTES: Readonly<Record<string, string>> = {
|
|||
"/builds": "/jobs",
|
||||
"/review": "/jobs",
|
||||
"/schedules": "/messages",
|
||||
"/notifications": "/swarm",
|
||||
};
|
||||
|
||||
/** Where an old address goes, keeping its query; `null` for any other path. */
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue