Public: anyone can read this; the crew can change it.
The /pipeline page speaks the home page's language #9838
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/9835-the-pipeline-page-speaks-the-home-page-s → main 2 commits fork
2 files
changed +68
−40
src/web/pages/pipeline.ts +33 −40
| @@ -13,7 +13,7 @@ import { notePhase, takeSessionTime } from "../../core/route_timing.ts"; | ||
| 13 | 13 | import { html } from "../html.ts"; |
| 14 | 14 | import { page } from "../layout.ts"; |
| 15 | 15 | import { reportPath } from "../../checks/report.ts"; |
| 16 | import { card, row, stat } from "../ui.ts"; | |
| 16 | import { stat } from "../ui.ts"; | |
| 17 | 17 | |
| 18 | 18 | /** What a pull waits on at each step, what only that step catches, and the parts inside it. */ |
| 19 | 19 | export const STEP_DETAIL: Record<Step, { waits: string; catches: string; inside: readonly string[] }> = { |
| @@ -77,48 +77,41 @@ export function pipelinePage(ctx: Ctx): Response { | ||
| 77 | 77 | one it is at and, in one sentence, why it waits there. |
| 78 | 78 | </p> |
| 79 | 79 | </div> |
| 80 | <ol class="list" id="path"> | |
| 80 | <div id="path"> | |
| 81 | 81 | ${STEPS.map((s, i) => { |
| 82 | 82 | const d = STEP_DETAIL[s]; |
| 83 | return row({ | |
| 84 | id: s, | |
| 85 | data: { step: s }, | |
| 86 | title: `${i + 1}. ${title(s)}`, | |
| 87 | meta: html`${STEP_MEANS[s]}`, | |
| 88 | fold: { | |
| 89 | id: `inside-${s}`, | |
| 90 | open: false, | |
| 91 | body: html`<div class="card-body"> | |
| 92 | <p class="small">It waits for ${d.waits} Only this step catches ${d.catches}</p> | |
| 93 | <ul class="small">${d.inside.map((line) => html`<li>${line}</li>`)}</ul> | |
| 94 | <p class="small muted">Measured, last ${STEP_WINDOW_DAYS} days, public repositories: median time, then how often red.</p> | |
| 95 | <div class="stats" data-measures="${s}">${STEP_MEASURES[s].map((k) => { | |
| 96 | const m = stats.get(k); | |
| 97 | if (m === undefined) return ""; | |
| 98 | const p = statParts(m); | |
| 99 | return stat({ label: m.label, value: p.value, title: m.medianMs === null ? "no median" : `median ${p.value}`, note: p.note, noteTitle: p.note }); | |
| 100 | })}</div> | |
| 101 | </div>`, | |
| 102 | }, | |
| 103 | }); | |
| 83 | return html`<section class="mb" id="${s}" data-step="${s}"> | |
| 84 | <h2>${i + 1}. ${title(s)}</h2> | |
| 85 | <p>${STEP_MEANS[s]}</p> | |
| 86 | <details id="inside-${s}"> | |
| 87 | <summary class="small">What is inside</summary> | |
| 88 | <p class="small">It waits for ${d.waits} Only this step catches ${d.catches}</p> | |
| 89 | <ul class="small">${d.inside.map((line) => html`<li>${line}</li>`)}</ul> | |
| 90 | <p class="small muted">Measured, last ${STEP_WINDOW_DAYS} days, public repositories: median time, then how often red.</p> | |
| 91 | <div class="stats" data-measures="${s}">${STEP_MEASURES[s].map((k) => { | |
| 92 | const m = stats.get(k); | |
| 93 | if (m === undefined) return ""; | |
| 94 | const p = statParts(m); | |
| 95 | return stat({ label: m.label, value: p.value, title: m.medianMs === null ? "no median" : `median ${p.value}`, note: p.note, noteTitle: p.note }); | |
| 96 | })}</div> | |
| 97 | </details> | |
| 98 | </section>`; | |
| 104 | 99 | })} |
| 105 | </ol> | |
| 106 | ${card( | |
| 107 | html`<div class="card-body"> | |
| 108 | <p class="small"> | |
| 109 | The whole test suite does not stand in the path: a pull's check runs the tests its change reaches, | |
| 110 | and the rest of the path runs lint and types. An admin sees how many changes sit at each step right now on | |
| 111 | <a href="/boards">Boards</a>. | |
| 112 | </p> | |
| 113 | <p class="small" id="full-suite"> | |
| 114 | The whole suite still runs, as a report: after a landing, on a schedule, against the default branch's tip. | |
| 115 | Each test file it finds failing gets one issue labelled <span class="mono">full-suite</span>, listed | |
| 116 | <a href="${reportPath()}">here</a>. The report holds no pull, batch or release. | |
| 117 | </p> | |
| 118 | <p class="muted small">For agents: the MCP tool <span class="mono">pull</span> answers <span class="mono">step: { step, why, done }</span> with these four words.</p> | |
| 119 | </div>`, | |
| 120 | { head: html`<h2>Where to look next</h2>` }, | |
| 121 | )}`; | |
| 100 | </div> | |
| 101 | <section class="mb"> | |
| 102 | <h2>Where to look next</h2> | |
| 103 | <p class="small"> | |
| 104 | The whole test suite does not stand in the path: a pull's check runs the tests its change reaches, | |
| 105 | and the rest of the path runs lint and types. An admin sees how many changes sit at each step right now on | |
| 106 | <a href="/boards">Boards</a>. | |
| 107 | </p> | |
| 108 | <p class="small" id="full-suite"> | |
| 109 | The whole suite still runs, as a report: after a landing, on a schedule, against the default branch's tip. | |
| 110 | Each test file it finds failing gets one issue labelled <span class="mono">full-suite</span>, listed | |
| 111 | <a href="${reportPath()}">here</a>. The report holds no pull, batch or release. | |
| 112 | </p> | |
| 113 | <p class="muted small">For agents: the MCP tool <span class="mono">pull</span> answers <span class="mono">step: { step, why, done }</span> with these four words.</p> | |
| 114 | </section>`; | |
| 122 | 115 | const response = page({ ctx, title: "How a change lands", body }); |
| 123 | 116 | notePhase(ctx.request, "render", performance.now() - rendering); |
| 124 | 117 | return response; |
test/pipeline-language.test.ts added +35
| @@ -0,0 +1,35 @@ | ||
| 1 | /** /pipeline speaks the home page's language (#9835): one heading, four plain sections, no card. */ | |
| 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 3 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 4 | import { tmpdir } from "node:os"; | |
| 5 | import { join } from "node:path"; | |
| 6 | import { createApp } from "../src/app.ts"; | |
| 7 | import { serve } from "../src/http/server.ts"; | |
| 8 | import { STEPS } from "../src/flow/path.ts"; | |
| 9 | ||
| 10 | let dir = ""; | |
| 11 | let app: ReturnType<typeof createApp>; | |
| 12 | let servers: Awaited<ReturnType<typeof serve>>; | |
| 13 | ||
| 14 | beforeAll(async () => { | |
| 15 | dir = await mkdtemp(join(tmpdir(), "forge-pipeline-lang-")); | |
| 16 | app = createApp({ dataDir: dir, port: 0 }); | |
| 17 | servers = await serve(app); | |
| 18 | }); | |
| 19 | afterAll(async () => { | |
| 20 | servers.stop(); | |
| 21 | app.close(); | |
| 22 | await rm(dir, { recursive: true, force: true }); | |
| 23 | }); | |
| 24 | ||
| 25 | test("/pipeline signed out has one h1, a heading per step and no card", async () => { | |
| 26 | const res = await fetch(servers.url + "/pipeline"); | |
| 27 | expect(res.status).toBe(200); | |
| 28 | const page = await res.text(); | |
| 29 | const main = page.slice(page.indexOf("<main"), page.indexOf("</main>")); | |
| 30 | expect(main.match(/<h1[ >]/g)?.length).toBe(1); | |
| 31 | expect(main).not.toContain('class="card'); | |
| 32 | expect(main).not.toContain("card-body"); | |
| 33 | for (const s of STEPS) expect(main).toMatch(new RegExp(`<h2>\\d\\. ${s.charAt(0).toUpperCase()}${s.slice(1)}</h2>`)); | |
| 34 | expect(main).toContain('href="/boards"'); | |
| 35 | }); |