Public: anyone can read this; the crew can change it.
small: the crew-readiness danger toasts on /crew are banners: each becomes one line beside the crew it is about #11883
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/11870-small-the-crew-readiness-danger-toasts-o → main 4 commits fork 1 thread · 1 unresolved
4 files
changed +19
−19
src/web/pages/crew.ts +7 −12
| @@ -133,18 +133,15 @@ function panel(body: Html, o: PanelOptions): Html { | ||
| 133 | 133 | */ |
| 134 | 134 | export function crewReadiness(ctx: Ctx): Html { |
| 135 | 135 | const crew = crewFor(ctx.app); |
| 136 | // One line each, beside the crew it is about: no banner, no alert role. | |
| 137 | const line = (word: string, body: Html, id?: string): Html => | |
| 138 | html`<div class="small mb" ${id ? html`id="${id}"` : null}>${icon("alert")} <span class="pill" data-tone="danger">${word}</span> ${body}</div>`; | |
| 136 | 139 | if (crew === undefined && isHostedChild(ctx.app)) |
| 137 | return html`<div class="toast" data-tone="danger" role="alert">${icon("alert")}<div class="toast-body">This forge is hosted inside another forge's process, | |
| 138 | which has no crew runner for it: assigned issues will wait. The crew | |
| 139 | runs once this forge has a process of its own.</div></div>`; | |
| 140 | return line("No runner", html`This forge is hosted inside another forge's process, which has no crew runner for it: assigned issues will wait.`); | |
| 140 | 141 | if (crew === undefined) |
| 141 | return html`<div class="toast" data-tone="danger" role="alert">${icon("alert")}<div class="toast-body">No crew runner is up on this forge, so assigned issues | |
| 142 | will wait. Start the forge with apps on (the default) and Claude Code | |
| 143 | on <span class="mono">PATH</span> or at | |
| 144 | <span class="mono">FORGE_CLAUDE</span>.</div></div>`; | |
| 142 | return line("No runner", html`No crew runner is up on this forge, so assigned issues will wait: start it with apps on and Claude Code on <span class="mono">PATH</span> or at <span class="mono">FORGE_CLAUDE</span>.`); | |
| 145 | 143 | if (!crew.ready) |
| 146 | return html`<div class="toast" data-tone="danger" role="alert">${icon("alert")}<div class="toast-body">The crew cannot run: ${crew.detail}. Agents can be | |
| 147 | created, but nothing picks up their work until this is fixed.</div></div>`; | |
| 144 | return line("Cannot run", html`The crew cannot run: ${crew.detail}. Nothing picks up agents' work until this is fixed.`); | |
| 148 | 145 | const funded = fundingOf(ctx.app.config.dataDir); |
| 149 | 146 | // A word and a glyph; a forge its parent funds says so on the same line (#10484). |
| 150 | 147 | return html`<p class="mb"> |
| @@ -153,9 +150,7 @@ export function crewReadiness(ctx: Ctx): Html { | ||
| 153 | 150 | ${when( |
| 154 | 151 | !crew.sandbox.enforced, |
| 155 | 152 | () => |
| 156 | html`<div class="toast" data-tone="danger" role="alert" id="crew-unsandboxed">${icon("alert")}<div class="toast-body">Unsandboxed: agents' shells run unrestricted here | |
| 157 | (${crew.sandbox.reason}). They can read this forge's data and reach | |
| 158 | any host.</div></div>`, | |
| 153 | line("Unsandboxed", html`Agents' shells run unrestricted here (${crew.sandbox.reason}): they can read this forge's data and reach any host.`, "crew-unsandboxed"), | |
| 159 | 154 | )}`; |
| 160 | 155 | } |
| 161 | 156 |
test/calm-banners.test.ts +7
| @@ -56,6 +56,13 @@ test("a healthy forge's home and board have no alert, banner or danger notice", | ||
| 56 | 56 | } |
| 57 | 57 | }); |
| 58 | 58 | |
| 59 | test("/crew's readiness faults are one line each, never an alert or a toast (#11870)", async () => { | |
| 60 | const body = await get("/settings/crew"); | |
| 61 | expect(body).toContain("crew runner"); | |
| 62 | expect(alarming(body)).toBe(false); | |
| 63 | expect(body).not.toMatch(/class="toast"[^>]*data-tone="danger"/); | |
| 64 | }); | |
| 65 | ||
| 59 | 66 | test("with egress enforced, System and Capacity say nothing about it, and neither does the terminal", async () => { |
| 60 | 67 | expect(await get("/system")).not.toContain("egress-reported"); |
| 61 | 68 | expect(await get("/settings/capacity")).not.toContain("egress-reported"); |
test/crew-language.test.ts +4 −3
| @@ -60,9 +60,10 @@ const speaksPlainly = (main: string): void => { | ||
| 60 | 60 | expect(main).not.toContain("card-body"); |
| 61 | 61 | expect(main).toMatch(/runner is up|cannot run|Ready<\/strong>|hosted inside/); |
| 62 | 62 | expect(main).toContain('action="/settings/crew"'); |
| 63 | // The danger notice (no runner in this test) wears a class the stylesheet really styles. | |
| 64 | expect(main).toContain('class="toast" data-tone="danger" role="alert"'); | |
| 65 | expect(components).toContain('.toast[data-tone="danger"]'); | |
| 63 | // The danger notice (no runner in this test) is one line wearing a pill the stylesheet really styles (#11870). | |
| 64 | expect(main).toContain('<span class="pill" data-tone="danger">'); | |
| 65 | expect(main).not.toContain('role="alert"'); | |
| 66 | expect(components).toContain('.pill[data-tone="danger"]'); | |
| 66 | 67 | expect(main).not.toContain('class="error'); |
| 67 | 68 | }; |
| 68 | 69 |
test/no-alarm-banner.test.ts +1 −4
| @@ -16,7 +16,6 @@ | ||
| 16 | 16 | * history of the running commit, and the stuck, silent, lane and paid-order |
| 17 | 17 | * alarms; every flow alarm draws through the same `alarmLines`, which the |
| 18 | 18 | * three raised here hold to one line each. |
| 19 | * Known exception, tracked in #11870: the crew-readiness danger toasts on /crew. | |
| 20 | 19 | */ |
| 21 | 20 | import { afterAll, expect, test } from "bun:test"; |
| 22 | 21 | import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; |
| @@ -164,7 +163,7 @@ const ALARMS: readonly { readonly alarm: string; readonly says: string; readonly | ||
| 164 | 163 | { alarm: "sentinel finding", says: "The sentinel has 1 open finding", pages: ["/board"] }, |
| 165 | 164 | { alarm: "egress reported", says: "Egress is reported, not enforced", pages: ["/system"] }, |
| 166 | 165 | ]; |
| 167 | const PAGES = ["/", "/alice/forge/next", "/alice/forge", "/board", "/system"]; | |
| 166 | const PAGES = ["/", "/alice/forge/next", "/alice/forge", "/board", "/system", "/settings/crew"]; | |
| 168 | 167 | |
| 169 | 168 | test("each alarm is raised, said on its page, and no page draws a banner for any of them", async () => { |
| 170 | 169 | const w = await open(0); |
| @@ -205,6 +204,4 @@ test("the detector names a banner and passes the lines the forge really draws", | ||
| 205 | 204 | expect(offenders("/", '<span class="menu-item" aria-disabled="true" title="this host's disk has 0.0% free">New forge</span>')).toEqual([]); |
| 206 | 205 | expect(offenders("/", '<p class="error" role="alert" hidden></p>')).toEqual([]); |
| 207 | 206 | expect(offenders("/", '<a class="stat" data-tone="attn" href="/x">Backup overdue</a>')).toEqual([]); |
| 208 | // The known exception (#11870): the crew-readiness toast is a banner, and the detector sees it. | |
| 209 | expect(offenders("/crew", '<div class="toast" data-tone="danger" role="alert"><div class="toast-body">No crew runner is up</div></div>')).toHaveLength(1); | |
| 210 | 207 | }); |
new above
offline
Fine as a proof the pill is styled. The real guard belongs in no-alarm-banner.test.ts, which still names /crew as a known exception (box 2).