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

The board shows the forge's health as one line: “all fine”, or what needs you, live #358

Merged opened this

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

Merged

scout/forge:scout/345-the-board-shows-the-forge-s-health-as-on → main 1 commit fork

  • Merged. scout/345-the-board-shows-the-forge-s-health-as-on was merged into main as bab479a.

Review

Sign in to review this change.
2 files changed +126 −0
src/web/pages/board.ts +50
@@ -13,6 +13,9 @@ import {
1313 type Decision,
1414 type DecisionKind,
1515 } from "../../core/decisions.ts";
16 import { appsRuntime } from "../../apps/runtime.ts";
17 import { listApps } from "../../core/apps.ts";
18 import { vitals, type Vitals } from "../../kernel/vitals.ts";
1619 import { getIssue, getIssuesByIds, type Issue } from "../../core/issues.ts";
1720 import { looksForApps } from "../../core/looks.ts";
1821 import { listPulls, mergedPullsForRepos, type Pull } from "../../core/pulls.ts";
@@ -172,6 +175,7 @@ export function boardPage(ctx: Ctx): Response {
172175 ></span>
173176 <time class="board-clock" id="board-clock"></time>
174177 </header>
178 ${forgeStrip(ctx)}
175179 <div class="board-grid">
176180 ${working(ctx, scope)} ${landed(ctx, scope)} ${decided(ctx, scope)}
177181 ${today(ctx, scope)}
@@ -217,6 +221,49 @@ function region(
217221 </section>`;
218222 }
219223
224 // ---- the forge ----
225
226 /** Where a need can be acted on, when it can: a stuck run's page. */
227 function needHref(need: string, v: Vitals): string | undefined {
228 if (/ stuck\.$/.test(need) && v.stuck[0] !== undefined)
229 return `/${v.stuck[0].repo}/runs/${v.stuck[0].runId}`;
230 return undefined;
231 }
232
233 /**
234 * The forge's health as one line, for a site admin only: *All fine*, or
235 * each thing that needs them. A live region of its own; nobody else gets
236 * the element or pays for the reads.
237 */
238 function forgeStrip(ctx: Ctx): Html | null {
239 if (!ctx.auth.user?.isAdmin) return null;
240 const app = ctx.app;
241 const runtime = appsRuntime(app);
242 const v = vitals({
243 db: app.db,
244 dataDir: app.config.dataDir,
245 envs: runtime
246 ? listApps(app.db).flatMap((record) => runtime.status(record))
247 : [],
248 });
249 return html`<div
250 class="board-forge"
251 id="forge"
252 data-live="run.*,app.*"
253 data-ok="${v.ok ? "yes" : "no"}"
254 role="status"
255 >
256 ${v.ok
257 ? html`<span class="board-forge-ok">All fine</span>`
258 : v.needs.map((need) => {
259 const href = needHref(need, v);
260 return href === undefined
261 ? html`<span class="board-forge-need">${need}</span>`
262 : html`<a class="board-forge-need" href="${href}">${need}</a>`;
263 })}
264 </div>`;
265 }
266
220267 // ---- working now ----
221268
222269 function working(ctx: Ctx, scope: BoardScope): Html {
@@ -519,6 +566,9 @@ export const boardCss = /* css */ `
519566 .board-live[data-state="open"] .board-dot { background: var(--success); box-shadow: 0 0 0 3px color-mix(in srgb, var(--success) 22%, transparent); }
520567 .board-live[data-state="reconnecting"] .board-dot { background: var(--attn); }
521568 .board-clock { font-family: var(--mono); font-size: 13px; font-variant-numeric: tabular-nums; color: var(--muted-fg); min-width: 5ch; text-align: right; }
569 .board-forge { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 16px; min-height: 28px; padding: 6px var(--pad) 0; font-size: 13px; color: var(--faint); }
570 .board-forge[data-ok="no"] { color: var(--attn); }
571 a.board-forge-need { color: inherit; text-decoration: underline; text-underline-offset: 3px; }
522572 .board-grid { flex: 1; display: grid; gap: var(--pad); padding: 12px var(--pad) var(--pad); min-height: 0; }
523573 .board-region { min-width: 0; min-height: 0; display: flex; flex-direction: column; }
524574 .board-h {
test/board.test.ts +76
@@ -280,3 +280,79 @@ describe("the board", () => {
280280 expect(home).toContain("Board");
281281 });
282282 });
283
284 describe("the board's forge strip", () => {
285 let dataDir: string;
286 let app: App;
287 let servers: Servers;
288 let admin: User;
289 let cookies: Record<string, string> = {};
290
291 async function signIn(name: string): Promise<string> {
292 const res = await fetch(`${servers.url}/signin`, {
293 method: "POST",
294 body: new URLSearchParams({
295 username: name,
296 password: "a-good-passphrase",
297 }),
298 redirect: "manual",
299 });
300 return (res.headers.get("set-cookie") ?? "").split(";")[0]!;
301 }
302 const board = async (who: string) =>
303 (
304 await fetch(`${servers.url}/board`, {
305 headers: { cookie: cookies[who]! },
306 })
307 ).text();
308
309 beforeAll(async () => {
310 dataDir = await mkdtemp(join(tmpdir(), "forge-board-strip-"));
311 app = createApp({ dataDir, port: 0 });
312 servers = await serve(app);
313 for (const [username, adminIfFirst] of [
314 ["root", true],
315 ["bob", false],
316 ] as const) {
317 const u = await createUser(app.db, {
318 username,
319 email: `${username}@forge.local`,
320 password: "a-good-passphrase",
321 adminIfFirst,
322 });
323 if (!u.ok) throw new Error(u.error);
324 if (username === "root") admin = u.value;
325 cookies[username] = await signIn(username);
326 }
327 });
328
329 afterAll(async () => {
330 servers.stop();
331 app.close();
332 await rm(dataDir, { recursive: true, force: true });
333 });
334
335 test("an admin reads All fine; a stuck run becomes a linked sentence; a non-admin sees nothing", async () => {
336 const fine = await board("root");
337 expect(fine).toMatch(/id="forge"\s+data-live="run\.\*,app\.\*"/);
338 expect(fine).toContain("All fine");
339
340 const made = await createRepo(app, admin, { name: "stuck" });
341 if (!made.ok) throw new Error(String(made.error));
342 const run = createRun(app.db, { repoId: made.value.id, kind: "agent" });
343 claimRun(app.db, run.id);
344 // Its last heartbeat is long past: the lease has lapsed.
345 app.db
346 .query("UPDATE run SET heartbeat_at = ?1 WHERE id = ?2")
347 .run("2020-01-01T00:00:00.000Z", run.id);
348 const stuck = await board("root");
349 expect(stuck).not.toContain("All fine");
350 expect(stuck).toContain("1 run is stuck.");
351 expect(stuck).toContain(`href="/root/stuck/runs/${run.id}"`);
352
353 const other = await board("bob");
354 expect(other).not.toContain('id="forge"');
355 expect(other).not.toContain("All fine");
356 expect(other).not.toContain("stuck.");
357 });
358 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs