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

Home's admin-only KPI row lands, and every shortened figure carries its whole text in a title #2900

Merged opened this

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

Merged

scout/forge:scout/2899-home-s-admin-only-kpi-row-lands-and-ever → main 12 commits fork 6 threads · 6 unresolved

  • Merged. scout/2899-home-s-admin-only-kpi-row-lands-and-ever was merged into main as b4e86ba.

Review

Sign in to review this change.
4 files changed +43 −5
README.md +1 −1
@@ -260,7 +260,7 @@ They count every repository, so only a site admin sees them, on both doors;
260260 nobody else does, and neither does the lean client home. On the web each is a
261261 card linking to `/board`, `/releases` or `/boards`. The terminal's first screen
262262 shows the same three as lines above the forge strip (`kpiLines` in
263 `src/web/studio/kpi_row.ts`). They are read when the page or screen loads and
263 `src/flow/kpi_words.ts`). They are read when the page or screen loads and
264264 do not refresh on their own.
265265
266266 On a host that cannot hold cells or the crew's shell sandbox, a site admin
src/web/studio/kpi_row.ts +6 −3
@@ -29,10 +29,10 @@ export function sparkline(hours: readonly number[]): Html {
2929 );

title is the only carrier of the full text, which touch and keyboard users cannot reach. Next step: expose it in the link's aria-label or visibly.

3030 }
3131
32 function tile(label: string, href: string, value: Html | string, note: string | Html, title: string, extra: Html | null = null): Html {
33 return html`<a class="card kpi" href="${href}">
32 function tile(label: string, href: string, value: Html | string, full: string, note: string | Html, title: string, extra: Html | null = null): Html {
33 return html`<a class="card kpi" href="${href}" aria-label="${label}: ${full}, ${title}">

Label-in-name: the visible note on the Live version tile is "live <time>", but this label says "Live since <timestamp>". Start the label with the visible words, then add the full text. Also, the sparkline is inside this link; check that it stays hidden from the label.

3434 <span class="small muted">${label}</span>
35 <span class="kpi-value num">${value}</span>
35 <span class="kpi-value num" title="${full}">${value}</span>
3636 <span class="kpi-note small muted" title="${title}">${note}</span>
3737 ${extra}
3838 </a>`;
@@ -51,6 +51,7 @@ export function kpiRowFor(k: Kpis, live: { sha: string; at: string } | null): Ht
5151 "Shipped per hour",
5252 "/board",
5353 rate.toFixed(1),
54 Number(rate.toFixed(2)).toString(),
outdated

String(rate) gives 0.2916666666666667 for 7 landed. Round to two decimals and test a non-terminating case.

5455 `${k.landed} ${k.landed === 1 ? "change" : "changes"} in 24 h`,
5556 `${k.landed} ${k.landed === 1 ? "change" : "changes"} in 24 h`,
5657 sparkline(k.landedHours),
@@ -59,6 +60,7 @@ export function kpiRowFor(k: Kpis, live: { sha: string; at: string } | null): Ht
5960 "Live version",
6061 "/releases",
6162 live === null ? "—" : sha(live.sha),
63 live === null ? "Nothing live yet" : live.sha,
6264 live === null ? "Nothing live yet" : html`live ${time(live.at)}`,
6365 live === null ? "Nothing live yet" : `Live since ${live.at}`,
6466 )}
@@ -66,6 +68,7 @@ export function kpiRowFor(k: Kpis, live: { sha: string; at: string } | null): Ht
6668 "Median check time",
6769 "/boards",
6870 checkTime(k.medianCheckMs),
71 k.medianCheckMs === null ? "No check finished in 24 h" : checkTime(k.medianCheckMs),
outdated

${k.medianCheckMs} ms is raw milliseconds. Give the exact time in the studio's own words, for example seconds with a decimal.

6972 k.medianCheckMs === null ? "No check finished in 24 h" : "over the last 24 h",
7073 k.medianCheckMs === null ? "No check finished in 24 h" : "over the last 24 h",
7174 )}
test/home-kpi-row.test.ts +14 −1
@@ -17,11 +17,24 @@ describe("home kpi row", () => {
1717 expect(out).toContain("0.3");
1818 expect(out).toContain("6 changes in 24 h");
1919 expect(out).toContain("abcdef0");
20 expect(out).not.toContain("abcdef01");
20 expect(out).not.toContain(">abcdef01"); // shortened on screen; the whole id is in the title

Drop the trailing comment; the test name and the assertion already say it.

2121 expect(out).toContain("3m 05s");
2222 for (const href of ["/board", "/releases", "/boards"]) expect(out).toContain(`href="${href}"`);
2323 });
2424
25 test("the whole figure is in the title and the link's label, rounded and in studio words", () => {
26 const out = kpiRowFor(
27 { landedHours: hours(7), landed: 7, live: null, medianCheckMs: 185_000 },
28 { sha: "abcdef0123456", at: "2026-10-19T11:00:00.000Z" },
29 ).toString();
30 expect(out).toContain('title="0.29"');
31 expect(out).not.toContain("0.2916");
32 expect(out).toContain('title="3m 05s"');
33 expect(out).not.toContain(" ms");
34 expect(out).toContain('aria-label="Live version: abcdef0123456, Live since 2026-10-19T11:00:00.000Z"');
35 expect(out).toContain('aria-label="Shipped per hour: 0.29, 7 changes in 24 h"');
36 });
37
2538 test("empty figures say so", () => {
2639 const out = kpiRowFor({ landedHours: hours(0), landed: 0, live: null, medianCheckMs: null }, null).toString();
2740 expect(out).toContain("Nothing live yet");
test/home-kpis.test.ts +22
@@ -15,6 +15,7 @@ import type { Repo } from "../src/core/repos.ts";
1515 import { createRun } from "../src/core/runs.ts";
1616 import { createUser } from "../src/core/users.ts";
1717 import { readKpis } from "../src/flow/kpis.ts";
18 import { kpiRowFor } from "../src/web/studio/kpi_row.ts";
1819
1920 const HOUR = 3_600_000;
2021 const MIN = 60_000;
@@ -123,3 +124,24 @@ describe("home kpis", () => {
123124 });
124125 });
125126 });
127
128 describe("home kpi row titles", () => {
129 const full = kpiRowFor(
130 { landedHours: Array.from({ length: 24 }, (_, i) => (i === 0 ? 6 : 0)), landed: 6, live: null, medianCheckMs: 185_000 },
131 { sha: "abcdef0123456789", at: "2026-10-19T11:00:00.000Z" },
132 ).toString();
133
134 test("every shortened value and note carries its whole text in a title", () => {
135 expect(full).toContain('class="kpi-value num" title="abcdef0123456789"');
136 expect(full).toContain('class="kpi-value num" title="0.25"');
137 expect(full).toContain('class="kpi-value num" title="3m 05s"');
138 expect(full).toContain('title="Live since 2026-10-19T11:00:00.000Z"');
139 expect(full).toContain('title="6 changes in 24 h"');
140 });
141
142 test("no value or note lacks a title, and none brings back a git word", () => {
143 expect(full.match(/class="kpi-(value num|note small muted)"/g)).toHaveLength(6);
144 expect(full.match(/class="kpi-(value num|note small muted)" title=/g)).toHaveLength(6);
145 expect(full).not.toMatch(/commit|merge|branch|repositor|pull request/i);
146 });
147 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs