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

An app's policy line draws §5's row and badge, and .policy-line leaves the stylesheet #4778

Merged opened this

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

Merged

scout/forge:scout/4757-an-app-s-policy-line-draws-5-s-row-and-b → main 10 commits fork 4 threads · 4 unresolved

  • Merged. scout/4757-an-app-s-policy-line-draws-5-s-row-and-b was merged into main as cd128e9.

Review

Sign in to review this change.
4 files changed +82 −36
docs/design/sections/5d-page-local-classes.md −1
@@ -53,7 +53,6 @@ the same line (#4888).
5353 - `.filters`
5454 - `.owner`
5555 - `.page-head`
56 - `.policy-line`
5756 - `.repo-actions`
5857 - `.repo-desc`
5958 - `.slash`
src/web/studio/app.ts +45 −25
@@ -39,6 +39,7 @@ import { liveClientJs, liveMount } from "../live.ts";
3939 import { followingCard, followingOf } from "../pages/following.ts";
4040 import {
4141 avatar,
42 badge,
4243 btn,
4344 card,
4445 envPhrase,
@@ -159,6 +160,11 @@ function previewLatencyWords(
159160 <span class="muted">· ${split}${latency.n} deploys${failedSuffix}</span>`;
160161 }
161162
163 /** The policy line's card: `setting-list` lets the title wrap and the trail drop under it on a phone. */
164 function policyLineCard(line: Html): Html {
outdated

Not blocking: src/web/pages/audit.ts has a policyCard too, and it draws the audit table. Two functions with one name and different jobs will confuse a grep. Rename this one, or add a class option to listCard and delete this helper.

165 return card(html`<ul class="list setting-list">${line}</ul>`, { pad: false });
166 }
167
162168 /**
163169 * One line about the app's policy, when it has one: what it opens to
164170 * anyone and what the running app said when the forge last knocked, with
@@ -174,14 +180,14 @@ async function policyLine(
174180 if (standing.kind === "unread") return html``;
175181 const href = `/audit#${repo.ownerName}-${repo.name}`;
176182 if (standing.kind === "unsound")
177 return html`<p class="policy-line">
178 ${icon("alert")}
179 <span
180 ><b>Its contract does not read</b>, so an app built on it does not
181 boot.</span
182 >
183 <a href="${href}">See why</a>
184 </p>`;
183 return policyLineCard(
184 row({
185 lead: icon("alert"),
186 title: html`<b>Its contract does not read</b>, so an app built on it
187 does not boot.`,
188 trail: html`<a href="${href}">See why</a>`,
189 }),
190 );
185191 const last = latestProbes(ctx.app.db, [record.id]).get(record.id);
186192 const open = standing.open.length;
187193 const said =
@@ -194,23 +200,37 @@ async function policyLine(
194200 : last.verdict === "down"
195201 ? "was not running when knocked on"
196202 : "has no door to knock on";
197 return html`<p class="policy-line" data-tone="${last?.verdict ?? "none"}">
198 ${icon("shield")}
199 <span
200 ><b
201 >${Object.keys(standing.contract.resources).length === 1
202 ? "One resource"
203 : `${Object.keys(standing.contract.resources).length} resources`}</b
204 >,
205 ${open === 0
206 ? "nothing open to anyone"
207 : open === 1
208 ? "one door open to anyone"
209 : `${open} doors open to anyone`};
210 ${said}.</span
211 >
212 <a href="${href}">The table</a>
213 </p>`;
203 const resources = Object.keys(standing.contract.resources).length;
204 const tone =
205 last?.verdict === "ok"
206 ? "success"
207 : last?.verdict === "mismatch"
208 ? "danger"
209 : "default";
210 const short =
211 last === undefined
212 ? "not knocked"
213 : last.verdict === "ok"
outdated

This trail is a nowrap sentence badge plus a link in a flex: none container, so it crowds the title out at 360px. Put the row in a list setting-list so the trail drops under the text below 640px.

214 ? "obeys"
215 : last.verdict === "mismatch"
216 ? "disobeys"
217 : last.verdict === "down"
218 ? "down"
219 : "no door";
220 return policyLineCard(
221 row({
222 lead: icon("shield"),
223 title: html`<b>${resources === 1 ? "One resource" : `${resources} resources`}</b>,
224 ${open === 0
225 ? "nothing open to anyone"
226 : open === 1
227 ? "one door open to anyone"
228 : `${open} doors open to anyone`}`,
229 meta: html`${said}`,
outdated

Format this multi-line title as prettier would. It is correct but oddly broken. Optional.

230 trail: html`${badge(short, { tone })}
231 <a href="${href}">The table</a>`,
232 }),
233 );
214234 }
215235
216236 export async function studioApp(
src/web/style/content.ts −8
@@ -205,14 +205,6 @@ a.origin-chip:hover { border-color: var(--border-strong); color: var(--fg); }
205205 .fg-s6 { fill: var(--series-6); color: var(--series-6); }
206206 .fg-s7 { fill: var(--series-7); color: var(--series-7); }
207207 .restore-command { margin: 8px 0 0; padding: 8px 10px; font-size: 12.5px; white-space: pre-wrap; word-break: break-all; background: var(--muted); border-radius: var(--radius); }
208 .policy-line {
209 display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
210 margin: 12px 0 0; font-size: 13px; color: var(--muted-fg);
211 }
212 .policy-line .ico { color: var(--faint); flex: none; }
213 .policy-line b { color: var(--fg); font-weight: 600; }
214 .policy-line[data-tone="mismatch"] .ico { color: var(--danger); }
215 .policy-line[data-tone="ok"] .ico { color: var(--success); }
216208 form.inline { display: inline; margin-left: auto; }
217209
218210 /* The studio's front door: the public apps, live first, as pictures. */
test/audit.test.ts +37 −2
@@ -442,7 +442,8 @@ describe("the audit reads every app's policy from git", () => {
442442 headers: { cookie: `${cookie}; forge_mode=studio` },
443443 })
444444 ).text();
445 expect(notes).toContain('class="policy-line"');
445 expect(notes).not.toContain("policy-line");
446 expect(notes).toContain('class="badge"');
446447 expect(notes).toContain("2 resources");
447448 expect(notes).toContain('href="/audit#alice-notes"');
448449 // The audit page carries that anchor.
@@ -456,6 +457,40 @@ describe("the audit reads every app's policy from git", () => {
456457 headers: { cookie: `${cookie}; forge_mode=studio` },
457458 })
458459 ).text();
459 expect(bare).not.toContain('class="policy-line"');
460 expect(bare).not.toContain("policy-line");
461 expect(bare).not.toContain("The table");
462 });
463
464 test("an app's policy line badges the last verdict: success when it obeys, danger when it does not", async () => {
465 const notes = getRepo(app.db, "alice", "notes")!;
466 const record = getAppByRepo(app.db, notes.id)!;
467 const page = async () =>
468 (
469 await fetch(`${servers.url}/alice/notes`, {
470 headers: { cookie: `${cookie}; forge_mode=studio` },
471 })
472 ).text();
473 const probe = (verdict: "ok" | "mismatch", said: string[]) =>
474 recordProbe(app.db, record.id, {
475 repo: notes,
476 commitOid: "c".repeat(40),
477 at: "",
478 verdict,
479 knocks: [],
480 said,
481 });
482 probe("ok", []);
483 const ok = await page();
484 expect(ok).toContain('data-variant="success"');
485 expect(ok).toContain("obeys its table");
486 expect(ok).toContain("setting-list");
487 expect(ok).toMatch(/data-variant="success"\s*>\s*(<svg[\s\S]*?<\/svg>)?obeys</);
488 expect(ok).not.toContain("policy-line");
489 probe("mismatch", ["note.delete as anon: the table says no, the app said 404"]);
490 const bad = await page();
491 expect(bad).toContain('data-variant="danger"');
492 expect(bad).toContain("one door disobeys its table");
493 expect(bad).toMatch(/data-variant="danger"\s*>\s*(<svg[\s\S]*?<\/svg>)?disobeys</);
494 expect(bad).not.toContain("policy-line");
460495 });
461496 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs