Public: anyone can read this; the crew can change it.
The /resilience page, each protection and the test that holds it, speaks the home page's language #9880
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/9877-the-resilience-page-each-protection-and → main 2 commits fork
3 files
changed +63
−8
docs/design/areas/resilience.md +2 −1
| @@ -1,7 +1,8 @@ | ||
| 1 | 1 | # Resilience |
| 2 | 2 | |
| 3 | 3 | `/resilience` (`src/web/pages/resilience.ts`) is a forge admin's: the backups line, the nightly jobs, a tally, and one |
| 4 | `row()` per protection in THREATS.md inside a `card` `.list`, its standing a `badge` word in the trail. | |
| 4 | `row()` per protection in THREATS.md in a plain `<section>` under an `<h2>`, rows in the `.list` with no card around | |
| 5 | them (#9877), its standing a `badge` word in the trail. | |
| 5 | 6 | |
| 6 | 7 | ## At 390 px (#9328) |
| 7 | 8 |
src/web/pages/resilience.ts +7 −7
| @@ -19,7 +19,7 @@ import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.t | ||
| 19 | 19 | import { notFoundPage, signInFirst } from "../chrome.ts"; |
| 20 | 20 | import { html, type Html } from "../html.ts"; |
| 21 | 21 | import { page } from "../layout.ts"; |
| 22 | import { badge, card } from "../ui.ts"; | |
| 22 | import { badge } from "../ui.ts"; | |
| 23 | 23 | |
| 24 | 24 | const TONE = { green: "success", red: "danger", untested: "default", "no test yet": "default" } as const; |
| 25 | 25 | |
| @@ -72,10 +72,11 @@ export const RESILIENCE_CSS = `[data-resilience] { overflow-wrap: anywhere; } | ||
| 72 | 72 | [data-resilience] .row-title, [data-resilience] .row-meta { white-space: normal; overflow: visible; text-overflow: clip; } |
| 73 | 73 | [data-resilience] .row { align-items: flex-start; }`; |
| 74 | 74 | |
| 75 | /** One row per protection, its standing a word in the trail. */ | |
| 75 | /** One row per protection under its heading, its standing a word in the trail; no card (#9877). */ | |
| 76 | 76 | export function protectionsHtml(rows: readonly Row[]): Html { |
| 77 | return card( | |
| 78 | html`<ul class="list"> | |
| 77 | return html`<section class="stack" aria-labelledby="protections-h"> | |
| 78 | <h2 id="protections-h">Protections</h2> | |
| 79 | <ul class="list"> | |
| 79 | 80 | ${rows.map( |
| 80 | 81 | ({ p, s }) => |
| 81 | 82 | html`<li class="row" data-standing="${s}"> |
| @@ -86,9 +87,8 @@ export function protectionsHtml(rows: readonly Row[]): Html { | ||
| 86 | 87 | <span class="row-trail">${badge(s, { tone: TONE[s] })}</span> |
| 87 | 88 | </li>`, |
| 88 | 89 | )} |
| 89 | </ul>`, | |
| 90 | { pad: false }, | |
| 91 | ); | |
| 90 | </ul> | |
| 91 | </section>`; | |
| 92 | 92 | } |
| 93 | 93 | |
| 94 | 94 | // GET /resilience: a forge admin's, since live red lines name what to attack. |
test/resilience-language.test.ts added +54
| @@ -0,0 +1,54 @@ | ||
| 1 | /** /resilience speaks the home page's language (#9877): one heading, the protections a plain section, no card. */ | |
| 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 3 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 4 | import { tmpdir } from "node:os"; | |
| 5 | import { join } from "node:path"; | |
| 6 | import type { Row } from "../src/checks/resilience.ts"; | |
| 7 | import { createApp, type App } from "../src/app.ts"; | |
| 8 | import { createSession } from "../src/core/sessions.ts"; | |
| 9 | import { createUser } from "../src/core/users.ts"; | |
| 10 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 11 | import { protectionsHtml } from "../src/web/pages/resilience.ts"; | |
| 12 | ||
| 13 | let dir = ""; | |
| 14 | let app: App; | |
| 15 | let servers: Servers; | |
| 16 | let cookie = ""; | |
| 17 | ||
| 18 | beforeAll(async () => { | |
| 19 | dir = await mkdtemp(join(tmpdir(), "forge-resilience-lang-")); | |
| 20 | app = createApp({ dataDir: dir, port: 0 }); | |
| 21 | servers = await serve(app); | |
| 22 | const u = await createUser(app.db, { | |
| 23 | origin: "seed", trusted: true, username: "alice", email: "[email protected]", password: "a-good-passphrase", adminIfFirst: true, | |
| 24 | }); | |
| 25 | if (!u.ok) throw new Error("user"); | |
| 26 | cookie = `forge_session=${createSession(app.db, u.value.id).token}`; | |
| 27 | }); | |
| 28 | afterAll(async () => { | |
| 29 | servers.stop(); | |
| 30 | app.close(); | |
| 31 | await rm(dir, { recursive: true, force: true }); | |
| 32 | }); | |
| 33 | ||
| 34 | const main = (page: string): string => page.slice(page.indexOf("<main"), page.indexOf("</main>")); | |
| 35 | ||
| 36 | test("an admin's /resilience: one h1, the protections a section under an h2, no card", async () => { | |
| 37 | const res = await fetch(`${servers.url}/resilience`, { headers: { cookie } }); | |
| 38 | expect(res.status).toBe(200); | |
| 39 | const page = main(await res.text()); | |
| 40 | expect(page.match(/<h1[\s>]/g)?.length).toBe(1); | |
| 41 | expect(page).not.toContain('class="card'); | |
| 42 | expect(page).not.toContain("card-body"); | |
| 43 | expect(page).toMatch(/<section class="stack" aria-labelledby="protections-h">\s*<h2 id="protections-h">Protections<\/h2>\s*<ul class="list">/); | |
| 44 | expect(page).toContain('<p id="backups-line"'); | |
| 45 | }); | |
| 46 | ||
| 47 | test("a cited file the last gate did not run reads untested, never green", () => { | |
| 48 | const rows: Row[] = [{ p: { adversary: "Anyone", text: "A protection", test: "test/never-run.test.ts" }, s: "untested" }]; | |
| 49 | const out = String(protectionsHtml(rows)); | |
| 50 | expect(out).toContain('data-standing="untested"'); | |
| 51 | expect(out).toContain(">untested<"); | |
| 52 | expect(out).not.toContain("success"); | |
| 53 | expect(out).not.toContain('class="card'); | |
| 54 | }); |