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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/9877-the-resilience-page-each-protection-and → main 2 commits fork

  • Merged. scout/9877-the-resilience-page-each-protection-and was merged into main as d6eeebd.

Review

Sign in to review this change.
3 files changed +63 −8
docs/design/areas/resilience.md +2 −1
@@ -1,7 +1,8 @@
11 # Resilience
22
33 `/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.
56
67 ## At 390 px (#9328)
78
src/web/pages/resilience.ts +7 −7
@@ -19,7 +19,7 @@ import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.t
1919 import { notFoundPage, signInFirst } from "../chrome.ts";
2020 import { html, type Html } from "../html.ts";
2121 import { page } from "../layout.ts";
22 import { badge, card } from "../ui.ts";
22 import { badge } from "../ui.ts";
2323
2424 const TONE = { green: "success", red: "danger", untested: "default", "no test yet": "default" } as const;
2525
@@ -72,10 +72,11 @@ export const RESILIENCE_CSS = `[data-resilience] { overflow-wrap: anywhere; }
7272 [data-resilience] .row-title, [data-resilience] .row-meta { white-space: normal; overflow: visible; text-overflow: clip; }
7373 [data-resilience] .row { align-items: flex-start; }`;
7474
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). */
7676 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">
7980 ${rows.map(
8081 ({ p, s }) =>
8182 html`<li class="row" data-standing="${s}">
@@ -86,9 +87,8 @@ export function protectionsHtml(rows: readonly Row[]): Html {
8687 <span class="row-trail">${badge(s, { tone: TONE[s] })}</span>
8788 </li>`,
8889 )}
89 </ul>`,
90 { pad: false },
91 );
90 </ul>
91 </section>`;
9292 }
9393
9494 // 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 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs