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

The Release keeper panel on /releases speaks the home page's language, no boxed card #10008

Merged opened this

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

Merged

scout/forge:scout/9998-the-release-keeper-panel-on-releases-spe → main 1 commit fork

  • Merged. scout/9998-the-release-keeper-panel-on-releases-spe was merged into main as 7024784.

Review

Sign in to review this change.
3 files changed +48 −4
docs/design/areas/releases.md +7
@@ -8,3 +8,10 @@ narrow screen a row's text column shrinks and its title and meta lines end in an
88 page wider. Free text on the page (an owner's hold and its reason, a keeper's reason) sits inside
99 `[data-releases]` (an attribute, not a class), which breaks long words anywhere. The hold's reason input is capped at
1010 100% of its box. The keeper panel's own table keeps the existing `.table-scroll` box. No fixed width. No new class.
11
12 ## The Release keeper, no card (#9998)
13
14 `keeperPanel()` in `src/web/pages/keeper-panel.ts` is a `<section class="settings-block" id="keeper">` with an
15 `<h2>Release keeper</h2>`, not a `.card`: one plain `#keeper-blocked` line saying what holds the next cut, the
16 releases table in its `.table-scroll`, and for an owner the Hold form and each owner hold's Lift form (same-origin
17 POSTs, unchanged). No new class. `test/keeper-language.test.ts` holds the shape.
src/web/pages/keeper-panel.ts +7 −4
@@ -2,6 +2,7 @@
22 * The release keeper on /releases (#7557): what stops the next cut, and
33 * each release it cut with its stage, the known-good version it rolls back
44 * to, and what the probes counted. Nothing when it has done nothing.
5 * A plain section under an h2, no card (#9998).
56 */
67
78 import type { Database } from "bun:sqlite";
@@ -10,7 +11,7 @@ import { secondsWords, withLandedToLive } from "../../keeper/landed.ts";
1011 import { listKeeperReleases, type Stage } from "../../keeper/record.ts";
1112 import { stallWords } from "../../perf/boot_stall.ts";
1213 import { html, when, type Html } from "../html.ts";
13 import { badge, btn, card, time, type Tone } from "../ui.ts";
14 import { badge, btn, time, type Tone } from "../ui.ts";
1415
1516 const STAGE: Record<Stage, { label: string; tone: Tone }> = {
1617 rolling: { label: "rolling", tone: "attn" },
@@ -19,7 +20,7 @@ const STAGE: Record<Stage, { label: string; tone: Tone }> = {
1920 rolled_back: { label: "rolled back", tone: "danger" },
2021 };
2122
22 /** `mayHold` is `mayRollBack`: the card then offers the owner's hold and its lift. */
23 /** `mayHold` is `mayRollBack`: the section then offers the owner's hold and its lift. */
2324 export function keeperPanel(db: Database, mayHold = false): Html | undefined {
2425 const releases = withLandedToLive(db, listKeeperReleases(db));
2526 const holds = standingHolds(db);
@@ -39,7 +40,8 @@ export function keeperPanel(db: Database, mayHold = false): Html | undefined {
3940 <td>${r.secondsDark} s</td>
4041 </tr>${when(r.reason !== "", () => html`<tr><td colspan="9" class="small muted">${r.reason}</td></tr>`)}`;
4142 });
42 return card(html`<h2>Release keeper</h2>
43 return html`<section class="settings-block" id="keeper">
44 <h2>Release keeper</h2>
4345 <p class="small ${blocked === undefined ? "muted" : ""}" id="keeper-blocked">${blocked ?? "Nothing holds the next cut."}</p>
4446 ${when(rows.length > 0, () => html`<div class="table-scroll" role="region" tabindex="0" aria-label="Keeper releases"><table class="table" data-sticky-lead>
4547 <thead><tr><th>Version</th><th>Cut</th><th>Landed to live</th><th>Known good</th><th>Roll non-200</th><th>Probes / pages</th><th>Worst</th><th>Stall, first 5 min</th><th>Dark</th></tr></thead>
@@ -50,5 +52,6 @@ export function keeperPanel(db: Database, mayHold = false): Html | undefined {
5052 )}
5153 <form method="post" action="/releases/hold" class="small">
5254 <label>Hold releases <input name="reason" maxlength="${MAX_REASON}" required placeholder="why"></label> ${btn("Hold", { type: "submit" })}
53 </form>`)}`);
55 </form>`)}
56 </section>`;
5457 }
test/keeper-language.test.ts added +34
@@ -0,0 +1,34 @@
1 /**
2 * The Release keeper on /releases speaks the home page's language (#9998):
3 * a plain section under an h2, no boxed card. What holds the next cut, the
4 * releases table and the owner's hold forms are unchanged.
5 */
6 import { expect, test } from "bun:test";
7 import { setHold } from "../src/keeper/hold.ts";
8 import { tick } from "../src/keeper/keeper.ts";
9 import { keeperPanel } from "../src/web/pages/keeper-panel.ts";
10 import { keeperForge } from "./helpers/keeper.ts";
11
12 const HEAD =
13 "<thead><tr><th>Version</th><th>Cut</th><th>Landed to live</th><th>Known good</th><th>Roll non-200</th><th>Probes / pages</th><th>Worst</th><th>Stall, first 5 min</th><th>Dark</th></tr></thead>";
14
15 test("the keeper is a section with an h2, no card, with its blocked line and its table", async () => {
16 const f = await keeperForge();
17 await tick(f.ports);
18 const out = String(keeperPanel(f.app.db));
19 expect(out).toMatch(/^<section[^>]*>\s*<h2>Release keeper<\/h2>/);
20 expect(out).not.toContain('class="card');
21 expect(out).toContain('id="keeper-blocked"');
22 expect(out).toContain("Nothing holds the next cut.");
23 expect(out).toContain(HEAD);
24 });
25
26 test("an owner's hold still shows with its same-origin Lift form, outside any card", async () => {
27 const f = await keeperForge();
28 const h = setHold(f.app.db, { kind: "owner", version: "", reason: "freeze for the demo", at: f.world.now });
29 const out = String(keeperPanel(f.app.db, true));
30 expect(out).not.toContain('class="card');
31 expect(out).toContain(`<form method="post" action="/releases/hold/${h.id}/lift"`);
32 expect(out).toContain('action="/releases/hold"');
33 expect(out).toMatch(/id="keeper-blocked">[^<]*freeze for the demo/);
34 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs