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

The terminal's dashboard lists every fired flow alarm in one line each #6512

Merged opened this

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

Merged

scout/forge:scout/6501-the-terminal-s-dashboard-lists-every-fir → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/6501-the-terminal-s-dashboard-lists-every-fir was merged into main as 9d53361.

Review

Sign in to review this change.
2 files changed +102 −0
src/tui/screens/dashboard.ts +24
@@ -100,6 +100,10 @@ import { listPinned } from "../../actions/board-pins.ts";
100100 import { migrateScreen } from "./migrate.ts";
101101 import { newRepoScreen } from "./newrepo.ts";
102102 import { forgeCeiling } from "../../flow/ceiling.ts";
103 import { alarmIdleCrew } from "../../flow/alarm_idle_crew.ts";
104 import { noReleaseAlarm } from "../../flow/alarm_no_release.ts";
105 import { redMainAlarm } from "../../flow/alarm_red_main.ts";
106 import { alarmStuck } from "../../flow/alarm_stuck.ts";
103107 import { pullScreen } from "./pull.ts";
104108 import { repoScreen } from "./repo.ts";
105109 import { local } from "./where.ts";
@@ -182,6 +186,8 @@ export function dashboardScreen(f: Session, ctx: Ctx): Screen {
182186 let decider: string | null = null;
183187 /** Home's three figures, as the web's row says them; empty unless an admin looks (they count every repository). */
184188 let figures: string[] = [];
189 /** One line per fired flow alarm; an admin reads every repository, anyone else public ones alone. */
190 let alarms: string[] = [];
185191 /** The panels this admin pinned to the web home, by title and board, in their order. */
186192 let panelLines: string[] = [];
187193 const filter = new Filter();
@@ -249,6 +255,17 @@ export function dashboardScreen(f: Session, ctx: Ctx): Screen {
249255 return hold === null ? [] : [[repo.id, holdWords(hold)] as const];
250256 }),
251257 );
258 alarms =

Four alarm reads run on every refresh; fine for now, but confirm the refresh stays well under 50 ms on a big database.

259 f.user?.isAdmin === true
260 ? [
261 alarmStuck(f.db, now),
262 redMainAlarm(f.db, now),
263 alarmIdleCrew(f.db, now, { ceilingUsd: f.app.config.crewCeilingUsd ?? null }),
264 noReleaseAlarm(f.db, now),
265 ]
266 .filter((a) => a.fired)
267 .map((a) => a.says)
268 : [alarmStuck(f.db, now, { publicOnly: true })].filter((a) => a.fired).map((a) => a.says);
252269 forge = forgeNeeds(f.app, f.user);
253270 if (forge !== undefined) {
254271 const fh = forgeHealth();
@@ -820,6 +837,13 @@ export function dashboardScreen(f: Session, ctx: Ctx): Screen {
820837 // A site admin's forge strip, as the board's: "All fine" or a line
821838 // per cause. Anyone else gets no rows and no reads.
822839 let body = area;
840 if (alarms.length > 0) {
841 const [strip, rest] = split(body, "v", [len(alarms.length), fill(1)]);
842 alarms.forEach((line, i) =>
843 s.text(strip!.x, strip!.y + i, truncate(line, strip!.w), style(c.attn, c.bg), strip!.x + strip!.w),
844 );
845 body = rest!;
846 }
823847 if (clientLines.length > 0) {
824848 const lines = clientLines.slice(0, 5);
825849 const [strip, rest] = split(body, "v", [len(lines.length), fill(1)]);
test/tui-dashboard-alarms.test.ts added +78
@@ -0,0 +1,78 @@
1 /** The terminal's dashboard draws each fired flow alarm in one line for an admin; anyone else reads public repositories alone (#6501). */
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, type App as Forge } from "../src/app.ts";
7 import { createUser, type User } from "../src/core/users.ts";
8 import { createForgeApp } from "../src/tui/forge.ts";
9 import { headlessTerminal } from "../src/tui/term/terminal.ts";
10 import { quietLogs } from "./helpers/globals.ts";
11 import { settleApp } from "./helpers/tui.ts";
12 import { seedAdmin } from "./helpers/admin.ts";
13
14 quietLogs();
15
16 let dir: string;
17 let forge: Forge;
18 let alice: User;
19 let bob: User;
20 const old = new Date(Date.now() - 3 * 86_400_000).toISOString();
21
22 beforeAll(async () => {
23 dir = await mkdtemp(join(tmpdir(), "forge-tui-alarms-"));
24 forge = createApp({ dataDir: dir, port: 0 });
25 const a = await seedAdmin(forge, { username: "alice", email: "[email protected]", password: "a-long-passphrase" });
26 if (!a.ok) throw new Error("signup failed");
27 alice = a.value;
28 const b = await createUser(forge.db, {
29 origin: "seed",
30 trusted: true,
31 username: "bob",
32 email: "[email protected]",
33 password: "a-long-passphrase",
34 });
35 if (!b.ok) throw new Error(b.error);
36 bob = b.value;
37 });
38
39 afterAll(async () => {
40 forge.close();
41 await rm(dir, { recursive: true, force: true });
42 });
43
44 async function firstScreen(user: User): Promise<string> {
45 const terminal = headlessTerminal(200, 32);
46 const { app } = createForgeApp(
47 forge,
48 { sshUsername: user.username, remoteAddress: "127.0.0.1", fingerprint: "SHA256:t", user },
49 { terminal, exitHooks: false },
50 );
51 app.start();
52 await settleApp(app);
53 const text = app.screen.toText();
54 app.stop();
55 return text;
56 }
57
58 /** An approved open pull in a new autonomous repository whose only event was three days ago. */
59 function stuckPull(name: string, visibility: "public" | "private"): void {
60 const db = forge.db;
61 db.run("INSERT INTO repo (id, owner_id, name, visibility, autonomy) VALUES (?1, ?2, ?1, ?3, 'auto')", [name, alice.id, visibility]);
62 db.run("INSERT INTO issue (id, repo_id, number, kind, title, author_id, state, created_at) VALUES (?1, ?1, 1, 'pull', 't', ?2, 'open', ?3)", [name, alice.id, old]);
63 db.run("INSERT INTO pull (issue_id, base_branch, head_branch, base_oid, head_oid) VALUES (?1, 'main', 'b', '', 'h')", [name]);
64 db.run("INSERT INTO review (id, issue_id, author_id, state, commit_oid, created_at) VALUES (?1, ?1, ?2, 'approved', 'h', ?3)", [name, alice.id, old]);
65 db.run("INSERT INTO run (id, repo_id, kind, issue_id, state, commit_oid, created_at, finished_at) VALUES (?1, ?1, 'check', ?1, 'failed', 'h', ?2, ?2)", [name, old]);
66 }
67
68 test("a quiet database draws no alarm line", async () => {
69 expect(await firstScreen(alice)).not.toContain("has sat approved");
70 });
71
72 test("a stuck approved pull draws the stuck line for an admin; a non-admin reads public ones alone", async () => {
73 stuckPull("secret", "private");
74 expect(await firstScreen(alice)).toContain("alice/secret#1 has sat approved");
75 expect(await firstScreen(bob)).not.toContain("secret#1");
76 stuckPull("open", "public");
77 expect(await firstScreen(bob)).toContain("alice/open#1 has sat approved");
78 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs