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

The week page speaks the home page's language #9779

Merged opened this

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

Merged

mason/forge:mason/9732-the-week-page-speaks-the-home-page-s-lan → main 4 commits fork 1 thread · 1 unresolved

  • Merged. mason/9732-the-week-page-speaks-the-home-page-s-lan was merged into main as 672fcfd.

Review

Sign in to review this change.
3 files changed +74 −52
src/web/pages/week.ts +58 −51
@@ -5,7 +5,7 @@
55 * and nothing else on the page changes anything.
66 */
77 import { takeProposal } from "../../actions/week.ts";
8 import { polishSpend, polishWords } from "../../crew/small_share.ts";
8 import { polishSpend } from "../../crew/small_share.ts";
99 import { canWrite, type Repo } from "../../core/repos.ts";
1010 import { notePhase, takeSessionTime } from "../../core/route_timing.ts";
1111 import { latestRetro, takenOf, type Line, type Retro } from "../../flow/retro.ts";
@@ -14,66 +14,74 @@ import { crumbsFor, loadRepo, notFoundPage, repoBase, repoHeader, repoPalette, s
1414 import { backTo, field, readForm } from "../form.ts";
1515 import { html, type Html } from "../html.ts";
1616 import { flashOrJson, page } from "../layout.ts";
17 import { btn, card, meter, row, stat } from "../ui.ts";
17 import { btn, meter, row } from "../ui.ts";
1818
1919 const lines = (base: string, ls: readonly Line[], none: string): Html =>
2020 ls.length === 0
2121 ? html`<p class="muted">${none}</p>`
22 : card(
23 html`<ul class="list setting-list">${ls.map((l) =>
24 row({ href: `${base}/issues/${l.number}`, title: l.title, meta: html`#${String(l.number)} · ${l.why}` }),
25 )}</ul>`,
26 { pad: false },
27 );
22 : html`<ul class="list setting-list">${ls.map((l) =>
23 row({ href: `${base}/issues/${l.number}`, title: l.title, meta: html`#${String(l.number)} · ${l.why}` }),
24 )}</ul>`;
25
26 /** A figure in the page's words, in the home page's type: `<strong class="num">`. */
27 const N = (n: number): Html => html`<strong class="num">${String(n)}</strong>`;
28
29 /** One section: the heading, then its words, on the spacing the settings pages use between sections. */
30 const block = (heading: string, ...body: (Html | null)[]): Html =>
31 html`<section class="settings-block"><h2>${heading}</h2>${body}</section>`;
2832
2933 function screen(base: string, retro: Retro, taken: ReadonlySet<number>, writer: boolean): Html {
3034 const p = retro.projected;
3135 const shipped = retro.shipped.length;
3236 const acc = retro.accuracy;
33 return html`<h2>What shipped</h2>
34 <p>${p === null
35 ? `${shipped} shipped. No projection was made for this week.`
36 : `${shipped} shipped against a projection of ${p.shipLow === p.shipHigh ? p.shipHigh : `${p.shipLow} to ${p.shipHigh}`} (${p.confidence} confidence).`}</p>
37 ${lines(base, retro.shipped, "Nothing shipped this week.")}
38 <h2>Estimate accuracy</h2>
39 <p>${acc.compared === 0
40 ? "No shipped issue had an estimate to compare."
41 : `${acc.inside} of ${acc.compared} shipped issues cost inside their estimated range.`}</p>
42 <h2>Cost most for least</h2>
43 ${lines(base, retro.costly, "Nothing open spent money this week.")}
44 <h2>Stuck</h2>
45 ${lines(base, retro.stuck, "Nothing is stuck.")}
46 <h2>Proposed changes</h2>
47 ${retro.proposals.length === 0
48 ? html`<p class="muted">Nothing to propose.</p>`
49 : html`<ul class="list setting-list">${retro.proposals.map((pr, idx) =>
50 row({
51 title: html`${pr.words}`,
52 meta: html`${taken.has(idx) ? "Taken." : writer ? "Nothing changes until you tap." : "Only someone who writes here can take it."}`,
53 ...(taken.has(idx) || !writer
54 ? {}
55 : {
56 after: html`<form method="post" action="${base}/week">
57 <input type="hidden" name="week_end" value="${retro.weekEnd}" />
58 <input type="hidden" name="idx" value="${String(idx)}" />
59 ${btn("Take it", { size: "sm" })}
60 </form>`,
61 }),
62 }),
63 )}</ul>`}`;
37 const proposals = retro.proposals;
38 return html`${block(
39 "What shipped",
40 html`<p>${p === null
41 ? html`${N(shipped)} shipped. No projection was made for this week.`
42 : html`${N(shipped)} shipped against a projection of ${p.shipLow === p.shipHigh ? N(p.shipHigh) : html`${N(p.shipLow)} to ${N(p.shipHigh)}`} <span class="muted">(${p.confidence} confidence)</span>.`}</p>`,
43 lines(base, retro.shipped, "Nothing shipped this week."),
44 )}
45 ${block(
46 "Estimate accuracy",
47 html`<p>${acc.compared === 0
48 ? "No shipped issue had an estimate to compare."
49 : html`${N(acc.inside)} of ${N(acc.compared)} shipped issues cost inside their estimated range.`}</p>`,
50 )}
51 ${block("Cost most for least", lines(base, retro.costly, "Nothing open spent money this week."))}
52 ${block("Stuck", lines(base, retro.stuck, "Nothing is stuck."))}
53 ${block(
54 "Proposed changes",
55 proposals.length === 0
56 ? html`<p class="muted">Nothing to propose.</p>`
57 : html`<p class="muted">${writer ? "Nothing changes until you tap." : "Only someone who writes here can take it."}</p>
58 <ul class="list setting-list">${proposals.map((pr, idx) =>
59 row({
60 title: html`${pr.words}`,
61 ...(taken.has(idx) ? { meta: html`Taken.` } : {}),
62 ...(taken.has(idx) || !writer
63 ? {}
64 : {
65 after: html`<form method="post" action="${base}/week">
66 <input type="hidden" name="week_end" value="${retro.weekEnd}" />
67 <input type="hidden" name="idx" value="${String(idx)}" />
68 ${btn("Take it", { size: "sm" })}
69 </form>`,
70 }),
71 }),
72 )}</ul>`,
73 )}`;
6474 }
6575
66 /** Polish spent against its share: a labelled fact, with the words the terminal and the tool print under it. */
67 function polishCard(ctx: Ctx, repo: Repo): Html {
76 /** Polish spent against its share, with the facts the terminal and the tool print. */
77 function polishBlock(ctx: Ctx, repo: Repo): Html {
6878 const p = polishSpend(ctx.app.db, repo.id, repo.polishShare, new Date());
69 return card(
70 html`<div data-polish>${stat({
71 label: "Polish share",
72 value: `${p.percent.toFixed(1)}% of ${p.share}%`,
73 title: "Agent spend in the last seven days on issues labelled polish or in the small-fixes lane, against all agent spend",
74 note: html`${polishWords(p)}`,
75 })}${meter(p.share === 0 ? 100 : (p.percent / p.share) * 100, `polish has used ${p.percent.toFixed(1)}% of the week's spend; its share is ${p.share}%`, p.under ? undefined : "attn")}</div>`,
76 );
79 const usd = (n: number) => `$${n.toFixed(2)}`;
80 return html`<div data-polish>${block(
81 "Polish share",
82 html`<p title="Agent spend in the last seven days on issues labelled polish or in the small-fixes lane, against all agent spend"><strong class="num">${p.percent.toFixed(1)}% of ${String(p.share)}%</strong> <span class="muted">(${usd(p.polishUsd)} of ${usd(p.totalUsd)}): ${p.under ? "ready polish may start" : "no polish starts until it is under"}</span></p>`,
83 meter(p.share === 0 ? 100 : (p.percent / p.share) * 100, `polish has used ${p.percent.toFixed(1)}% of the week's spend; its share is ${p.share}%`, p.under ? undefined : "attn"),
84 )}</div>`;
7785 }
7886
7987 export function weekPage(ctx: Ctx): Response {
@@ -91,8 +99,7 @@ export function weekPage(ctx: Ctx): Response {
9199 ? "The forge writes this when the weekly window closes. None has closed yet."
92100 : `Written when the window ended ${retro.weekEnd.slice(0, 10)}, once, and never edited.`} <a href="${base}/next">Next up</a></p>
93101 </div>
94 <h2>Polish this week</h2>
95 ${polishCard(ctx, repo)}
102 ${polishBlock(ctx, repo)}
96103 ${retro === undefined ? null : screen(base, retro, takenOf(ctx.app.db, repo.id, retro.weekEnd), writer)}`;
97104 const response = page({ ctx, title: `The week · ${repo.name}`, crumbs: crumbsFor(repo), palette: repoPalette(ctx, repo), body });
98105 notePhase(ctx.request, "render", performance.now() - rendering);
test/polish-share.test.ts +2 −1
@@ -201,6 +201,7 @@ test("the week page and the week tool show polish spent against the share", asyn
201201 spend(feature, 9, new Date(now.getTime() - 86_400_000));
202202 spend(polish, 1, new Date(now.getTime() - 86_400_000));
203203 const page = await (await fetch(`${servers.url}/alice/notes/week`, { headers: { cookie } })).text();
204 expect(page).toContain("polish 10.0% of 10% this week ($1.00 of $10.00)");
204 expect(page).toContain("10.0% of 10%");
205 expect(page).toContain("($1.00 of $10.00)");
205206 expect(page).toContain("no polish starts until it is under");
206207 });
test/week-doors.test.ts +14
@@ -291,6 +291,20 @@ describe("the week's doors", () => {
291291 expect(takenOf(forge.db, repo.id, retro.weekEnd).size).toBe(0);
292292 });
293293
294 test("the page is plain sections: no card around a list or the polish share, figures in strong.num, the note said once", async () => {
295 const res = await fetch(`${base}/alice/plan/week`, { headers: { cookie: aliceCookie } });
296 const body = await res.text();
297 const main = body.slice(body.indexOf("<h1>The week</h1>"));
298 expect(main).not.toMatch(/class="[^"]*\bcard\b/);
299 expect(main).not.toContain('class="stat');
300 expect(main).not.toContain('class="mt"');
301 expect(main).toContain('<section class="settings-block"><h2>Polish share</h2>');
302 expect(main).not.toContain("Polish this week");
303 expect(main).toMatch(/<strong class="num">\d+<\/strong> shipped/);
304 expect(main.split("Nothing changes until you tap.").length - 1).toBe(1);
305 expect(main).toContain("Take it");
306 });
307
294308 test("POST /week: no cookie, another origin, a reader and an agent are refused", async () => {
295309 const same = { origin: base };
296310 expect((await post({ ...same })).status).not.toBe(200);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs