Public: anyone can read this; the crew can change it.
The terminal's dashboard lists every fired flow alarm in one line each #6512
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6501-the-terminal-s-dashboard-lists-every-fir → main 1 commit fork 1 thread · 1 unresolved
2 files
changed +102
−0
src/tui/screens/dashboard.ts +24
| @@ -100,6 +100,10 @@ import { listPinned } from "../../actions/board-pins.ts"; | ||
| 100 | 100 | import { migrateScreen } from "./migrate.ts"; |
| 101 | 101 | import { newRepoScreen } from "./newrepo.ts"; |
| 102 | 102 | 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"; | |
| 103 | 107 | import { pullScreen } from "./pull.ts"; |
| 104 | 108 | import { repoScreen } from "./repo.ts"; |
| 105 | 109 | import { local } from "./where.ts"; |
| @@ -182,6 +186,8 @@ export function dashboardScreen(f: Session, ctx: Ctx): Screen { | ||
| 182 | 186 | let decider: string | null = null; |
| 183 | 187 | /** Home's three figures, as the web's row says them; empty unless an admin looks (they count every repository). */ |
| 184 | 188 | let figures: string[] = []; |
| 189 | /** One line per fired flow alarm; an admin reads every repository, anyone else public ones alone. */ | |
| 190 | let alarms: string[] = []; | |
| 185 | 191 | /** The panels this admin pinned to the web home, by title and board, in their order. */ |
| 186 | 192 | let panelLines: string[] = []; |
| 187 | 193 | const filter = new Filter(); |
| @@ -249,6 +255,17 @@ export function dashboardScreen(f: Session, ctx: Ctx): Screen { | ||
| 249 | 255 | return hold === null ? [] : [[repo.id, holdWords(hold)] as const]; |
| 250 | 256 | }), |
| 251 | 257 | ); |
| 258 | alarms = | |
| 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); | |
| 252 | 269 | forge = forgeNeeds(f.app, f.user); |
| 253 | 270 | if (forge !== undefined) { |
| 254 | 271 | const fh = forgeHealth(); |
| @@ -820,6 +837,13 @@ export function dashboardScreen(f: Session, ctx: Ctx): Screen { | ||
| 820 | 837 | // A site admin's forge strip, as the board's: "All fine" or a line |
| 821 | 838 | // per cause. Anyone else gets no rows and no reads. |
| 822 | 839 | 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 | } | |
| 823 | 847 | if (clientLines.length > 0) { |
| 824 | 848 | const lines = clientLines.slice(0, 5); |
| 825 | 849 | 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 | }); |
new above
offline
Four alarm reads run on every refresh; fine for now, but confirm the refresh stays well under 50 ms on a big database.