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

Design: /audit is built from the design system, and its audit-* classes leave §5d #3217

Merged opened this

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

Merged

mason/forge:mason/3196-design-audit-is-built-from-the-design-sy → main 1 commit fork

  • Merged. mason/3196-design-audit-is-built-from-the-design-sy was merged into main as 2e49f71.

Review

Sign in to review this change.
4 files changed +123 −133
docs/DESIGN.md +1 −1

Diff not shown — a line 2,427 characters wide (limit 2,000). +1 −1

View file
src/web/pages/audit.ts +110 −104
@@ -37,7 +37,17 @@ import { loadRepo, notFoundPage, signInFirst } from "../chrome.ts";
3737 import { readForm } from "../form.ts";
3838 import { html, when, type Html } from "../html.ts";
3939 import { flashTo, page } from "../layout.ts";
40 import { badge, btn, card, empty, icon, sha, time } from "../ui.ts";
40 import {
41 badge,
42 btn,
43 card,
44 empty,
45 icon,
46 listCard,
47 row,
48 sha,
49 time,
50 } from "../ui.ts";
4151
4252 const AUDIT_LIMIT = 200;
4353
@@ -110,17 +120,15 @@ export async function auditPage(ctx: Ctx): Promise<Response> {
110120 title: "No apps to audit",
111121 note: "An app's policy shows here once it has a contract.",
112122 })
113 : html`<p class="audit-sum">
123 : html`<h2 class="mb">
114124 ${summary(sound.length, open, unsound.length, unread.length)}
115 ${when(
116 disobeying.length > 0,
117 () =>
118 html` <span class="audit-alarm"
119 >${count(disobeying.length, "app disobeys", "apps disobey")}
120 its table.</span
121 >`,
125 ${when(disobeying.length > 0, () =>
126 badge(
127 `${count(disobeying.length, "app disobeys", "apps disobey")} its table.`,
128 { tone: "danger", icon: "alert" },
129 ),
122130 )}
123 </p>
131 </h2>
124132 ${unsound.map(unsoundCard)}
125133 ${sound.map((a) =>
126134 policyCard(
@@ -177,7 +185,7 @@ function policyCard(
177185 );
178186 return card(
179187 html`<div class="table-scroll">
180 <table class="table audit" data-dense>
188 <table class="table" data-dense>
181189 <caption class="sr-only">
182190 ${name(a.repo)}: what each identity may do to each resource
183191 </caption>
@@ -195,7 +203,7 @@ function policyCard(
195203 html`<tr>
196204 ${i === 0
197205 ? html`<th scope="rowgroup" rowspan="${verbs.length}">
198 ${resource}
206 <code>${resource}</code>
199207 </th>`
200208 : null}
201209 <th scope="row">${verb}</th>
@@ -210,26 +218,21 @@ function policyCard(
210218 )}
211219 </tbody>
212220 </table>
213 </div>`,
221 </div>
222 ${when(last?.verdict === "mismatch", () => sentences(last!.said))}`,
214223 {
215224 pad: false,
216225 id: `${a.repo.ownerName}-${a.repo.name}`,
217226 ...(last?.verdict === "mismatch" ? { tone: "danger" as const } : {}),
218227 foot: probeFoot(a, last, writer),
219 head: html`<a href="/${a.repo.ownerName}/${a.repo.name}" class="audit-app"
220 >${icon("repo")} ${name(a.repo)}</a
221 >
222 <span class="audit-trail">
223 ${a.standing.open.length === 0
224 ? badge("closed to strangers", { tone: "success" })
225 : badge(
226 `${count(a.standing.open.length, "door", "doors")} open to anyone`,
227 { tone: "attn" },
228 )}
229 ${a.oid === undefined
230 ? null
231 : html`<code class="muted">${a.oid.slice(0, 7)}</code>`}
232 </span>`,
228 head: html`${appHead(a.repo, "repo")}
229 ${a.standing.open.length === 0
230 ? badge("closed to strangers", { tone: "success" })
231 : badge(
232 `${count(a.standing.open.length, "door", "doors")} open to anyone`,
233 { tone: "attn" },
234 )}
235 ${a.oid === undefined ? null : sha(a.oid)}`,
233236 },
234237 );
235238 }
@@ -272,21 +275,11 @@ function probeFoot(
272275 : last.verdict === "down"
273276 ? html`${badge("was not running", { tone: "attn" })}`
274277 : html`${badge("no door to knock on")}`;
275 return html`<span class="audit-said"
276 >${said}
277 <span class="muted small">
278 asked ${time(last.createdAt)} at
279 <code>${last.commitOid.slice(0, 7)}</code></span
280 ></span
278 return html`${said}
279 <span class="muted small">
280 asked ${time(last.createdAt)} at ${sha(last.commitOid)}</span
281281 >
282 ${knock}
283 ${when(
284 last.verdict === "mismatch",
285 () =>
286 html`<ul class="audit-problems audit-wide">
287 ${last.said.map((s) => html`<li>${s}</li>`)}
288 </ul>`,
289 )}`;
282 ${knock}`;
290283 }
291284
292285 /**
@@ -397,44 +390,59 @@ export async function auditProbeSubmit(ctx: Ctx): Promise<Response> {
397390 });
398391 }
399392
393 /** A cell's answer as a §5 badge: `yes` lets anyone through, so it is the loud one. */
400394 function answer(a: Cell["answer"]): Html {
401 return html`<span class="audit-answer" data-answer="${a}">${a}</span>`;
395 return badge(a, {
396 ...(a === "yes" ? { tone: "attn" as const } : {}),
397 variant: a === "own" ? "secondary" : "outline",
398 });
399 }
400
401 /** A card's head names the app it is about, linked, as a §5 card head. */
402 function appHead(repo: Repo, mark: "repo" | "alert"): Html {
403 return html`${icon(mark)}
404 <h3><a href="/${repo.ownerName}/${repo.name}">${name(repo)}</a></h3>
405 <span class="spacer"></span>`;
406 }
407
408 /** Whole sentences — what is wrong, what a door said — as §5 prose. */
409 function sentences(lines: readonly string[]): Html {
410 return html`<div class="prose">
411 <ul>
412 ${lines.map((s) => html`<li>${s}</li>`)}
413 </ul>
414 </div>`;
402415 }
403416
404417 function unsoundCard(a: Audited): Html {
405418 if (a.standing.kind !== "unsound") return html``;
406 return card(
407 html`<ul class="audit-problems">
408 ${a.standing.problems.map((p) => html`<li>${p}</li>`)}
409 </ul>`,
410 {
411 tone: "danger",
412 id: `${a.repo.ownerName}-${a.repo.name}`,
413 head: html`<a href="/${a.repo.ownerName}/${a.repo.name}" class="audit-app"
414 >${icon("alert")} ${name(a.repo)}</a
415 >
416 <span class="audit-trail"
417 >${badge("contract unsound", { tone: "danger" })}</span
418 >`,
419 },
420 );
419 return card(sentences(a.standing.problems), {
420 pad: false,
421 tone: "danger",
422 id: `${a.repo.ownerName}-${a.repo.name}`,
423 head: html`${appHead(a.repo, "alert")}
424 ${badge("contract unsound", { tone: "danger" })}`,
425 });
421426 }
422427
423428 function unreadCard(unread: readonly Audited[]): Html {
424 return card(
425 html`<p class="muted small">
429 return listCard(
430 unread.map((a) =>
431 row({
432 href: `/${a.repo.ownerName}/${a.repo.name}`,
433 title: name(a.repo),
434 lead: icon("repo"),
435 nav: true,
436 }),
437 ),
438 {
439 head: html`${icon("eye")}
440 <h3>Unread</h3>`,
441 foot: html`<span class="muted small">
426442 These apps have no <code>contract.json</code>: their doors are written
427443 by hand, and this page cannot say what they allow.
428 </p>
429 <ul class="audit-unread">
430 ${unread.map(
431 (a) =>
432 html`<li>
433 <a href="/${a.repo.ownerName}/${a.repo.name}">${name(a.repo)}</a>
434 </li>`,
435 )}
436 </ul>`,
437 { head: html`<h3 class="audit-app">${icon("eye")} Unread</h3>` },
444 </span>`,
445 },
438446 );
439447 }
440448
@@ -447,7 +455,7 @@ function changesSection(
447455 ): Html {
448456 if (crossings.length === 0 && changes.length === 0 && verdicts.length === 0)
449457 return html``;
450 return html`<h2 class="audit-h2">What changed</h2>
458 return html`<h2 class="mt mb">What changed</h2>
451459 ${when(crossings.length > 0, () => crossingsCard(crossings))}
452460 ${when(changes.length > 0, () => changesCard(changes))}
453461 ${when(verdicts.length > 0, () => verdictsCard(verdicts))}`;
@@ -488,10 +496,10 @@ function crossingsCard(crossings: readonly Crossing[]): Html {
488496 {
489497 pad: false,
490498 tone: "attn",
491 head: html`<h3 class="audit-app">
492 ${icon("shield")} ${count(crossings.length, "pull waits", "pulls wait")}
493 at the wall
494 </h3>`,
499 head: html`${icon("shield")}
500 <h3>
501 ${count(crossings.length, "pull waits", "pulls wait")} at the wall
502 </h3>`,
495503 },
496504 );
497505 }
@@ -526,7 +534,7 @@ function changesCard(changes: readonly ContractChange[]): Html {
526534 ? "success"
527535 : "default";
528536 return html`<li>
529 <div class="row audit-change">
537 <div class="row">
530538 <span class="row-lead">${icon("commit")}</span>
531539 <span class="row-main">
532540 <span class="row-title">${name(c.repo)}: ${c.subject}</span>
@@ -537,21 +545,12 @@ function changesCard(changes: readonly ContractChange[]): Html {
537545 )}
538546 · ${c.author} · ${time(c.date)}</span
539547 >
540 ${when(
541 c.loosened.length + c.tightened.length > 0,
542 () =>
543 html`<ul class="audit-problems audit-cells">
544 ${c.loosened.map(
545 (l) => html`<li data-way="wider">${wallSentence(l)}</li>`,
546 )}
547 ${c.tightened.map(
548 (l) =>
549 html`<li data-way="narrower">
550 ${l.resource}.${l.verb} as ${l.as}: was ${l.to}, now
551 ${l.from === "absent" ? "gone" : l.from}
552 </li>`,
553 )}
554 </ul>`,
548 ${c.loosened.map((l) => cellLine("wider", wallSentence(l)))}
549 ${c.tightened.map((l) =>
550 cellLine(
551 "narrower",
552 `${l.resource}.${l.verb} as ${l.as}: was ${l.to}, now ${l.from === "absent" ? "gone" : l.from}`,
553 ),
555554 )}
556555 </span>
557556 <span class="row-trail">${badge(what, { tone })}</span>
@@ -561,10 +560,10 @@ function changesCard(changes: readonly ContractChange[]): Html {
561560 </ul>`,
562561 {
563562 pad: false,
564 head: html`<h3 class="audit-app">
565 ${icon("history")}
566 ${count(changes.length, "contract change", "contract changes")}
567 </h3>`,
563 head: html`${icon("history")}
564 <h3>
565 ${count(changes.length, "contract change", "contract changes")}
566 </h3>`,
568567 },
569568 );
570569 }
@@ -603,13 +602,7 @@ function verdictsCard(verdicts: readonly VerdictChange[]): Html {
603602 · ${time(v.at)} at
604603 <code>${v.commitOid.slice(0, 7)}</code></span
605604 >
606 ${when(
607 v.said.length > 0,
608 () =>
609 html`<ul class="audit-problems audit-cells">
610 ${v.said.map((s) => html`<li>${s}</li>`)}
611 </ul>`,
612 )}
605 ${v.said.map((s) => cellLine(undefined, s))}
613606 </span>
614607 </div>
615608 </li>`,
@@ -617,9 +610,22 @@ function verdictsCard(verdicts: readonly VerdictChange[]): Html {
617610 </ul>`,
618611 {
619612 pad: false,
620 head: html`<h3 class="audit-app">
621 ${icon("activity")} Doors, over time
622 </h3>`,
613 head: html`${icon("activity")}
614 <h3>Doors, over time</h3>`,
623615 },
624616 );
625617 }
618
619 /**
620 * One cell a change touched, as one more §5 row-meta line under the row's
621 * title: which way it moved, then the cell. The whole sentence is its title,
622 * so a line the row ellipsizes is still there to read.
623 */
624 function cellLine(way: "wider" | "narrower" | undefined, text: string): Html {
625 return html`<span
626 class="row-meta"
627 title="${text}"
628 ${way === undefined ? null : html`data-way="${way}"`}
629 >${way === undefined ? null : html`${way} · `}<code>${text}</code></span
630 >`;
631 }
src/web/style/content.ts −28
@@ -324,27 +324,6 @@ a.origin-chip:hover { border-color: var(--border-strong); color: var(--fg); }
324324 .frozen-form { display: flex; align-items: flex-end; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
325325 .frozen-form .field { margin: 0; min-width: 0; flex: 1 1 14rem; max-width: 22rem; }
326326
327 /* The audit: one app's policy as a table, an answer per cell. */
328 .audit-sum { margin: 0 0 14px; font-size: 15px; font-weight: 600; letter-spacing: -0.01em; text-wrap: balance; }
329 .audit-app { display: inline-flex; align-items: center; gap: 6px; font-weight: 600; color: var(--fg); margin: 0; font-size: 14px; }
330 .audit-app .ico { color: var(--faint); }
331 .audit-trail { display: inline-flex; align-items: center; gap: 8px; margin-left: auto; }
332 .audit-trail code { font-size: 11.5px; }
333 .audit th[scope="rowgroup"] { vertical-align: top; font-family: var(--mono); font-weight: 600; }
334 .audit th[scope="row"] { font-weight: 400; color: var(--muted-fg); }
335 .audit th[scope="rowgroup"], .audit th[scope="row"] { width: 1%; white-space: nowrap; padding-right: 24px; }
336 .audit thead th:nth-child(n + 3), .audit td { text-align: center; }
337 .audit-answer {
338 display: inline-block; min-width: 3.5ch; padding: 1px 6px; border-radius: 999px;
339 font-family: var(--mono); font-size: 11.5px; line-height: 18px;
340 }
341 .audit-answer[data-answer="yes"] { background: color-mix(in srgb, var(--attn) 18%, transparent); color: var(--attn); }
342 .audit-answer[data-answer="own"] { background: var(--muted); color: var(--fg); }
343 .audit-answer[data-answer="no"] { color: var(--faint); }
344 .audit-problems { margin: 0; padding-left: 18px; font-size: 13px; }
345 .audit-wide { flex-basis: 100%; }
346 .audit-said { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; }
347 .audit-alarm { color: var(--danger); }
348327 .wall-cells { margin: 8px 0 0; padding-left: 18px; font-family: var(--mono); font-size: 12.5px; }
349328 .grants th, .grants td { padding: 8px 12px; border-top: 1px solid var(--border); }
350329 .grants thead th { border-top: 0; font-weight: 600; }
@@ -463,15 +442,8 @@ a.origin-chip:hover { border-color: var(--border-strong); color: var(--fg); }
463442 .policy-line b { color: var(--fg); font-weight: 600; }
464443 .policy-line[data-tone="mismatch"] .ico { color: var(--danger); }
465444 .policy-line[data-tone="ok"] .ico { color: var(--success); }
466 .audit-h2 { font-size: 15px; font-weight: 600; letter-spacing: -0.01em; margin: 22px 0 10px; }
467 .audit-cells { margin-top: 6px; font-family: var(--mono); font-size: 12.5px; }
468 .audit-cells li[data-way="wider"] { color: var(--attn); }
469 .audit-cells li[data-way="narrower"] { color: var(--success); }
470 .audit-change .row-main { min-width: 0; }
471445 .wall-cells li + li { margin-top: 3px; }
472446 form.inline { display: inline; margin-left: auto; }
473 .audit-problems li + li { margin-top: 4px; }
474 .audit-unread { margin: 8px 0 0; padding: 0; list-style: none; display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 13px; }
475447
476448 /* The family tree: nesting is indentation and a rule, never a scrollbar. */
477449 .lineage-children {
test/audit.test.ts +12
@@ -207,6 +207,18 @@ 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.
211 expect(body).toMatch(
212 /<td data-answer="own">\s*<span\s+class="badge"\s+data-variant="secondary"\s*>own<\/span/,
213 );
214 expect(body).toContain('<table class="table" data-dense>');
215 expect(body).toMatch(
216 /<div class="card-head">[\s\S]*?<h3><a href="\/alice\/notes">alice\/notes<\/a><\/h3>/,
217 );
218 // The unread apps are §5 rows.
219 expect(body).toMatch(/<a href="\/alice\/bare"\s+class="row"/);
220 expect(body).not.toMatch(/class="[^"]*\baudit\b/);
221 expect(body).not.toContain("audit-");
210222 expect(body).toContain("2 doors open to anyone");
211223 // The summary counts the apps with a contract and the doors they open.
212224 expect(body).toContain(

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs