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

The /audit page, who can do what across every app, speaks the home page's language #9866

Merged opened this

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

Merged

scout/forge:scout/9863-the-audit-page-who-can-do-what-across-ev → main 2 commits fork

  • Merged. scout/9863-the-audit-page-who-can-do-what-across-ev was merged into main as e70b6ad.

Review

Sign in to review this change.
4 files changed +38 −29
docs/design/areas/lists.md +3 −2
@@ -39,8 +39,9 @@ one-line ellipsis. No fixed width; the page does not scroll sideways. No new cla
3939
4040 `/audit` (`auditPage` in `src/web/pages/audit.ts`) has no boxed card. Each app's standing (its policy table with the
4141 last probe under it, an unsound contract's problems, the Unread rows) and each "What changed" part (pulls at the wall,
42 contract changes, doors over time) is a `.card.bare`: an `<h3>` over its rows on the page's one background, with no
43 frame, no tone and no icon before the heading. The state is a badge's word ("contract unsound", "1 door disobeys",
42 contract changes, doors over time) is a `<section class="settings-block">` (#9863), no `card` at all: an `<h2>` (an
43 `<h3>` under "What changed") on a `.bar` line over its rows on the page's one background, with no frame, no tone and no
44 icon before the heading. The state is a badge's word ("contract unsound", "1 door disobeys",
4445 "at the wall"), so dropping the tone loses nothing. The table, the `id` each app section is linked by
4546 (`/audit#owner-name`) and the probe form do not change. No new class. Pinned by `test/audit.test.ts`.
4647
src/web/pages/audit.ts +25 −18
@@ -46,7 +46,6 @@ import { flashTo, page } from "../layout.ts";
4646 import {
4747 badge,
4848 btn,
49 card,
5049 empty,
5150 icon,
5251 row,
@@ -138,7 +137,7 @@ export async function auditPage(ctx: Ctx): Promise<Response> {
138137 title: "No apps to audit",
139138 note: "An app's policy shows here once it has a contract.",
140139 })
141 : html`<h2 class="mb">
140 : html`<p class="mb">
142141 ${summary(sound.length, open, unsound.length, unread.length)}
143142 ${when(disobeying.length > 0, () =>
144143 badge(
@@ -146,7 +145,7 @@ export async function auditPage(ctx: Ctx): Promise<Response> {
146145 { tone: "danger", icon: "alert" },
147146 ),
148147 )}
149 </h2>
148 </p>
150149 ${unsound.map(unsoundSection)}
151150 ${sound.map((a) =>
152151 policySection(
@@ -187,11 +186,17 @@ const name = (repo: Repo): string => `${repo.ownerName}/${repo.name}`;
187186
188187 /**
189188 * 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.
189 * `.settings-block` `<section>`, its `<h2>` on a `.bar` line (so a badge and
190 * a commit sit beside the name), its rows under it on the page's one
191 * background, its foot a second `.bar`. Its state is a badge's word, so it
192 * carries no tone.
192193 */
193 const section = (body: Html, o: { head: Html; foot?: Html; id?: string }) =>
194 card(body, { ...o, pad: false, extra: "bare" });
194 const part = (body: Html, o: { head: Html; foot?: Html; id?: string }) =>
195 html`<section class="settings-block" ${o.id ? html`id="${o.id}"` : null}>
196 <div class="bar mb">${o.head}</div>
197 ${body}
198 ${o.foot ? html`<div class="bar mt">${o.foot}</div>` : null}
199 </section>`;
195200
196201 /**
197202 * One app's policy as a table — resources × verbs, a column per identity —
@@ -211,7 +216,7 @@ function policySection(
211216 cells.find(
212217 (c) => c.resource === resource && c.verb === verb && c.as === as,
213218 );
214 return section(
219 return part(
215220 html`<div class="table-scroll">
216221 <table class="table" data-dense>
217222 <caption class="sr-only">
@@ -426,13 +431,13 @@ function answer(a: Cell["answer"]): Html {
426431
427432 /** A section's head names the app it is about, linked. */
428433 function appHead(repo: Repo): Html {
429 return html`<h3><a href="/${repo.ownerName}/${repo.name}">${name(repo)}</a></h3>
434 return html`<h2><a href="/${repo.ownerName}/${repo.name}">${name(repo)}</a></h2>
430435 <span class="spacer"></span>`;
431436 }
432437
433438 /** Whole sentences — what is wrong, what a door said — as §5 prose. */
434439 function sentences(lines: readonly string[]): Html {
435 return html`<div class="prose">
440 return html`<div class="prose prose-page">
436441 <ul>
437442 ${lines.map((s) => html`<li>${s}</li>`)}
438443 </ul>
@@ -441,7 +446,7 @@ function sentences(lines: readonly string[]): Html {
441446
442447 function unsoundSection(a: Audited): Html {
443448 if (a.standing.kind !== "unsound") return html``;
444 return section(sentences(a.standing.problems), {
449 return part(sentences(a.standing.problems), {
445450 id: `${a.repo.ownerName}-${a.repo.name}`,
446451 head: html`${appHead(a.repo)}
447452 ${badge("contract unsound", { tone: "danger" })}`,
@@ -449,7 +454,7 @@ function unsoundSection(a: Audited): Html {
449454 }
450455
451456 function unreadSection(unread: readonly Audited[]): Html {
452 return section(
457 return part(
453458 html`<ul class="list">
454459 ${unread.map((a) =>
455460 row({
@@ -461,7 +466,7 @@ function unreadSection(unread: readonly Audited[]): Html {
461466 )}
462467 </ul>`,
463468 {
464 head: html`<h3>Unread</h3>`,
469 head: html`<h2>Unread</h2>`,
465470 foot: html`<span class="muted small">
466471 These apps have no <code>contract.json</code>: their doors are written
467472 by hand, and this page cannot say what they allow.
@@ -479,15 +484,17 @@ function changesSection(
479484 ): Html {
480485 if (crossings.length === 0 && changes.length === 0 && verdicts.length === 0)
481486 return html``;
482 return html`<h2 class="mt mb">What changed</h2>
487 return html`<section class="settings-block">
488 <h2>What changed</h2>
483489 ${when(crossings.length > 0, () => crossingsSection(crossings))}
484490 ${when(changes.length > 0, () => contractChangesSection(changes))}
485 ${when(verdicts.length > 0, () => verdictsSection(verdicts))}`;
491 ${when(verdicts.length > 0, () => verdictsSection(verdicts))}
492 </section>`;
486493 }
487494
488495 /** Pulls the crew left at the wall: each is a person's decision, waiting. */
489496 function crossingsSection(crossings: readonly Crossing[]): Html {
490 return section(
497 return part(
491498 html`<ul class="list">
492499 ${crossings.map(
493500 (c) =>
@@ -524,7 +531,7 @@ function crossingsSection(crossings: readonly Crossing[]): Html {
524531
525532 /** Contract commits, newest first, each with what it did to the cells. */
526533 function contractChangesSection(changes: readonly ContractChange[]): Html {
527 return section(
534 return part(
528535 html`<ul class="list">
529536 ${changes.map((c) => {
530537 const what = c.removed
@@ -594,7 +601,7 @@ function verdictsSection(verdicts: readonly VerdictChange[]): Html {
594601 : v === "down"
595602 ? "was not running"
596603 : "had no door to knock on";
597 return section(
604 return part(
598605 html`<ul class="list">
599606 ${verdicts.map(
600607 (v) =>
src/web/style/components.ts +1 −1
@@ -982,7 +982,7 @@ details[open] > summary.row > .row-lead { transform: rotate(90deg); }
982982 /* ---- settings content: status rows, blocks, a segmented control ---- */
983983 .settings-block { margin-top: 24px; }
984984 .settings-block:first-child { margin-top: 0; }
985 .settings-block > h2, .settings-block > .bar > h2 { font-size: 14px; font-weight: 600; margin: 0; }
985 .settings-block > h2, .settings-block > .bar > :is(h2, h3) { font-size: 14px; font-weight: 600; margin: 0; }
986986 .settings-block > h2 { margin-bottom: 8px; }
987987 .settings-block > .bar { gap: 12px; flex-wrap: wrap; }
988988 .settings-block > p { margin-top: 8px; }
test/audit.test.ts +9 −8
@@ -213,20 +213,21 @@ describe("the audit reads every app's policy from git", () => {
213213 );
214214 expect(body).toContain('<table class="table" data-dense>');
215215 expect(body).toMatch(
216 /<div class="card-head"><h3><a href="\/alice\/notes">alice\/notes<\/a><\/h3>/,
216 /<div class="bar mb"><h2><a href="\/alice\/notes">alice\/notes<\/a><\/h2>/,
217217 );
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.
218 // Rows, not boxes (#9710, #9863): each app's standing is a <section>
219 // under its <h2>, its rows on the page's one background — no card at
220 // all, no tone, no icon before the name.
221221 for (const app of ["alice-notes", "alice-broken", "alice-secret"])
222222 expect(body).toMatch(
223 new RegExp(`<div\\s+class="card bare"\\s+id="${app}"\\s*>`),
223 new RegExp(`<section\\s+class="settings-block"\\s+id="${app}"\\s*>`),
224224 );
225225 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=/);
226 expect(main).not.toContain('class="card');
227 expect(main).not.toMatch(/data-tone=/);
228 // An app without a contract is not hidden: it is a row under "Unread".
228229 expect(main).toMatch(
229 /<div class="card-head"><h3>Unread<\/h3><\/div>\s*<ul class="list">/,
230 /<section\s+class="settings-block"\s*>\s*<div class="bar mb"><h2>Unread<\/h2><\/div>\s*<ul class="list">\s*<li[^>]*>\s*<a href="\/alice\/bare"\s+class="row"/,
230231 );
231232 // The unread apps are §5 rows.
232233 expect(body).toMatch(/<a href="\/alice\/bare"\s+class="row"/);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs