Public: anyone can read this; the crew can change it.

The /audit page speaks the home page's language #9729

Merged opened this

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

Merged

mason/forge:mason/9710-the-audit-page-speaks-the-home-page-s-la → main 2 commits fork

  • Merged. mason/9710-the-audit-page-speaks-the-home-page-s-la was merged into main as 385647c.

Review

Sign in to review this change.
3 files changed +63 −45
docs/design/areas/lists.md +9
@@ -26,3 +26,12 @@ The proofs under a forge on `/forges` (#7959) are `row()` `children`, one per pu
2626 one-line ellipsis. No fixed width; the page does not scroll sideways. No new class. The owner's phone look of
2727 `/preview-admin/sample/pulls` is attached to the pull from its preview.
2828 - `.archived-list` — the rows of an origin's archived repositories: long names wrap, and on a phone each row's buttons drop below it.
29
30 ## The audit as headings over rows (#9710)
31
32 `/audit` (`auditPage` in `src/web/pages/audit.ts`) has no boxed card. Each app's standing (its policy table with the
33 last probe under it, an unsound contract's problems, the Unread rows) and each "What changed" part (pulls at the wall,
34 contract changes, doors over time) is a `.card.bare`: an `<h3>` over its rows on the page's one background, with no
35 frame, no tone and no icon before the heading. The state is a badge's word ("contract unsound", "1 door disobeys",
36 "at the wall"), so dropping the tone loses nothing. The table, the `id` each app section is linked by
37 (`/audit#owner-name`) and the probe form do not change. No new class. Pinned by `test/audit.test.ts`.
src/web/pages/audit.ts +39 −43
@@ -49,7 +49,6 @@ import {
4949 card,
5050 empty,
5151 icon,
52 listCard,
5352 row,
5453 sha,
5554 time,
@@ -148,16 +147,16 @@ export async function auditPage(ctx: Ctx): Promise<Response> {
148147 ),
149148 )}
150149 </h2>
151 ${unsound.map(unsoundCard)}
150 ${unsound.map(unsoundSection)}
152151 ${sound.map((a) =>
153 policyCard(
152 policySection(
154153 a,
155154 probed.get(records.get(a.repo.id)!.id),
156155 ctx.auth.user !== undefined &&
157156 canWrite(ctx.app.db, a.repo, ctx.auth.user),
158157 ),
159158 )}
160 ${when(unread.length > 0, () => unreadCard(unread))}
159 ${when(unread.length > 0, () => unreadSection(unread))}
161160 ${changesSection(crossings, changes, verdicts)}`}`,
162161 });
163162 notePhase(req, "render", performance.now() - rendering);
@@ -186,12 +185,20 @@ function summary(
186185
187186 const name = (repo: Repo): string => `${repo.ownerName}/${repo.name}`;
188187
188 /**
189 * A part of the page as a heading over its rows, never a box: the shared
190 * `.card.bare` (docs/design/areas/code.md) keeps the card's markup and hooks
191 * and takes its frame off. Its state is a badge's word, so it carries no tone.
192 */
193 const section = (body: Html, o: { head: Html; foot?: Html; id?: string }) =>
194 card(body, { ...o, pad: false, extra: "bare" });
195
189196 /**
190197 * One app's policy as a table — resources × verbs, a column per identity —
191198 * with what the running app said last time the forge knocked, and, for
192199 * whoever can write it, a way to knock now.
193200 */
194 function policyCard(
201 function policySection(
195202 a: Audited,
196203 last: ProbeRow | undefined,
197204 writer: boolean,
@@ -204,7 +211,7 @@ function policyCard(
204211 cells.find(
205212 (c) => c.resource === resource && c.verb === verb && c.as === as,
206213 );
207 return card(
214 return section(
208215 html`<div class="table-scroll">
209216 <table class="table" data-dense>
210217 <caption class="sr-only">
@@ -242,11 +249,9 @@ function policyCard(
242249 </div>
243250 ${when(last?.verdict === "mismatch", () => sentences(last!.said))}`,
244251 {
245 pad: false,
246252 id: `${a.repo.ownerName}-${a.repo.name}`,
247 ...(last?.verdict === "mismatch" ? { tone: "danger" as const } : {}),
248253 foot: probeFoot(a, last, writer),
249 head: html`${appHead(a.repo, "repo")}
254 head: html`${appHead(a.repo)}
250255 ${a.standing.open.length === 0
251256 ? badge("closed to strangers", { tone: "success" })
252257 : badge(
@@ -419,10 +424,9 @@ function answer(a: Cell["answer"]): Html {
419424 });
420425 }
421426
422 /** A card's head names the app it is about, linked, as a §5 card head. */
423 function appHead(repo: Repo, mark: "repo" | "alert"): Html {
424 return html`${icon(mark)}
425 <h3><a href="/${repo.ownerName}/${repo.name}">${name(repo)}</a></h3>
427 /** A section's head names the app it is about, linked. */
428 function appHead(repo: Repo): Html {
429 return html`<h3><a href="/${repo.ownerName}/${repo.name}">${name(repo)}</a></h3>
426430 <span class="spacer"></span>`;
427431 }
428432
@@ -435,30 +439,29 @@ function sentences(lines: readonly string[]): Html {
435439 </div>`;
436440 }
437441
438 function unsoundCard(a: Audited): Html {
442 function unsoundSection(a: Audited): Html {
439443 if (a.standing.kind !== "unsound") return html``;
440 return card(sentences(a.standing.problems), {
441 pad: false,
442 tone: "danger",
444 return section(sentences(a.standing.problems), {
443445 id: `${a.repo.ownerName}-${a.repo.name}`,
444 head: html`${appHead(a.repo, "alert")}
446 head: html`${appHead(a.repo)}
445447 ${badge("contract unsound", { tone: "danger" })}`,
446448 });
447449 }
448450
449 function unreadCard(unread: readonly Audited[]): Html {
450 return listCard(
451 unread.map((a) =>
451 function unreadSection(unread: readonly Audited[]): Html {
452 return section(
453 html`<ul class="list">
454 ${unread.map((a) =>
452455 row({
453456 href: `/${a.repo.ownerName}/${a.repo.name}`,
454457 title: name(a.repo),
455458 lead: icon("repo"),
456459 nav: true,
457460 }),
458 ),
461 )}
462 </ul>`,
459463 {
460 head: html`${icon("eye")}
461 <h3>Unread</h3>`,
464 head: html`<h3>Unread</h3>`,
462465 foot: html`<span class="muted small">
463466 These apps have no <code>contract.json</code>: their doors are written
464467 by hand, and this page cannot say what they allow.
@@ -477,14 +480,14 @@ function changesSection(
477480 if (crossings.length === 0 && changes.length === 0 && verdicts.length === 0)
478481 return html``;
479482 return html`<h2 class="mt mb">What changed</h2>
480 ${when(crossings.length > 0, () => crossingsCard(crossings))}
481 ${when(changes.length > 0, () => changesCard(changes))}
482 ${when(verdicts.length > 0, () => verdictsCard(verdicts))}`;
483 ${when(crossings.length > 0, () => crossingsSection(crossings))}
484 ${when(changes.length > 0, () => contractChangesSection(changes))}
485 ${when(verdicts.length > 0, () => verdictsSection(verdicts))}`;
483486 }
484487
485488 /** Pulls the crew left at the wall: each is a person's decision, waiting. */
486 function crossingsCard(crossings: readonly Crossing[]): Html {
487 return card(
489 function crossingsSection(crossings: readonly Crossing[]): Html {
490 return section(
488491 html`<ul class="list">
489492 ${crossings.map(
490493 (c) =>
@@ -512,10 +515,7 @@ function crossingsCard(crossings: readonly Crossing[]): Html {
512515 )}
513516 </ul>`,
514517 {
515 pad: false,
516 tone: "attn",
517 head: html`${icon("shield")}
518 <h3>
518 head: html`<h3>
519519 ${count(crossings.length, "pull waits", "pulls wait")} at the wall
520520 </h3>`,
521521 },
@@ -523,8 +523,8 @@ function crossingsCard(crossings: readonly Crossing[]): Html {
523523 }
524524
525525 /** Contract commits, newest first, each with what it did to the cells. */
526 function changesCard(changes: readonly ContractChange[]): Html {
527 return card(
526 function contractChangesSection(changes: readonly ContractChange[]): Html {
527 return section(
528528 html`<ul class="list">
529529 ${changes.map((c) => {
530530 const what = c.removed
@@ -577,9 +577,7 @@ function changesCard(changes: readonly ContractChange[]): Html {
577577 })}
578578 </ul>`,
579579 {
580 pad: false,
581 head: html`${icon("history")}
582 <h3>
580 head: html`<h3>
583581 ${count(changes.length, "contract change", "contract changes")}
584582 </h3>`,
585583 },
@@ -587,7 +585,7 @@ function changesCard(changes: readonly ContractChange[]): Html {
587585 }
588586
589587 /** Where an app's doors started or stopped agreeing with its table. */
590 function verdictsCard(verdicts: readonly VerdictChange[]): Html {
588 function verdictsSection(verdicts: readonly VerdictChange[]): Html {
591589 const word = (v: VerdictChange["to"]): string =>
592590 v === "ok"
593591 ? "obeys its table"
@@ -596,7 +594,7 @@ function verdictsCard(verdicts: readonly VerdictChange[]): Html {
596594 : v === "down"
597595 ? "was not running"
598596 : "had no door to knock on";
599 return card(
597 return section(
600598 html`<ul class="list">
601599 ${verdicts.map(
602600 (v) =>
@@ -627,9 +625,7 @@ function verdictsCard(verdicts: readonly VerdictChange[]): Html {
627625 )}
628626 </ul>`,
629627 {
630 pad: false,
631 head: html`${icon("activity")}
632 <h3>Doors, over time</h3>`,
628 head: html`<h3>Doors, over time</h3>`,
633629 },
634630 );
635631 }
test/audit.test.ts +15 −2
@@ -207,13 +207,26 @@ describe("the audit reads every app's policy from git", () => {
207207 expect(body).toContain("Unread");
208208 // The table's cells, as the scaffold's contract says.
209209 expect(body).toMatch(/data-answer="own"/);
210 // …each answer a §5 badge, in a §5 table, under a §5 card head.
210 // …each answer a §5 badge, in a §5 table, under the app's heading.
211211 expect(body).toMatch(
212212 /<td data-answer="own">\s*<span\s+class="badge"\s+data-variant="secondary"\s*>own<\/span/,
213213 );
214214 expect(body).toContain('<table class="table" data-dense>');
215215 expect(body).toMatch(
216 /<div class="card-head">[\s\S]*?<h3><a href="\/alice\/notes">alice\/notes<\/a><\/h3>/,
216 /<div class="card-head"><h3><a href="\/alice\/notes">alice\/notes<\/a><\/h3>/,
217 );
218 // Rows, not boxes (#9710): each app's standing is a heading over its
219 // rows on the page's one background — a bare card, no frame, no tone,
220 // no icon before the name — and nothing on the page is a boxed card.
221 for (const app of ["alice-notes", "alice-broken", "alice-secret"])
222 expect(body).toMatch(
223 new RegExp(`<div\\s+class="card bare"\\s+id="${app}"\\s*>`),
224 );
225 const main = body.slice(body.indexOf("<h1>Audit</h1>"));
226 expect(main).not.toMatch(/class="card"/);
227 expect(main).not.toMatch(/class="card[^"]*"[^>]*data-tone=/);
228 expect(main).toMatch(
229 /<div class="card-head"><h3>Unread<\/h3><\/div>\s*<ul class="list">/,
217230 );
218231 // The unread apps are §5 rows.
219232 expect(body).toMatch(/<a href="\/alice\/bare"\s+class="row"/);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs