Public: anyone can read this; the crew can change it.
A warning never blanks the board or paints the home red: one line, where it belongs #1128
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/1109-a-warning-never-blanks-the-board-or-pain → main 1 commit fork 1 thread · 1 unresolved
6 files
changed +50
−22
src/kernel/vitals.ts +3 −10
| @@ -1,5 +1,7 @@ | ||
| 1 | 1 | /** |
| 2 | * The forge's standing: is it fine, and if not, what is off. Plain data in, | |
| 2 | * The forge's standing: is it fine, and if not, what is off. Only what needs | |
| 3 | * a person now is a need; a setting that is not the default is a note on its | |
| 4 | * own page (`web/pages/capacity.ts`), never here. Plain data in, | |
| 3 | 5 | * plain data out — the disk under `FORGE_DATA`, this process's memory, the |
| 4 | 6 | * queue, runs that stopped making progress, and how many apps are serving, |
| 5 | 7 | * asleep or previewed. A fixed handful of queries however many runs and |
| @@ -12,11 +14,6 @@ import { freemem, totalmem } from "node:os"; | ||
| 12 | 14 | import { recentErrors, type ErrorLine } from "./log.ts"; |
| 13 | 15 | import { leaseLapsed, listRunning, queueDepth } from "../core/runs.ts"; |
| 14 | 16 | import { STALE_AFTER_MS } from "../runs/lease.ts"; |
| 15 | import { egressSetting } from "../core/capacity.ts"; | |
| 16 | ||
| 17 | /** What every new cell may reach while egress is only reported. */ | |
| 18 | export const EGRESS_REPORTED = | |
| 19 | "Egress is reported, not enforced: every new app, preview, terminal and check cell may reach any public host on ports 80 and 443. Set it back to enforce under Settings → Capacity."; | |
| 20 | 17 | |
| 21 | 18 | /** Free disk under this share of the volume is something to say. */ |
| 22 | 19 | export const DISK_FREE_MIN = 0.1; |
| @@ -114,10 +111,6 @@ export function vitals(input: VitalsInput): Vitals { | ||
| 114 | 111 | `${seen.lastHour} errors in the last hour (${ERROR_BURST} or more is a burst).`, |
| 115 | 112 | ); |
| 116 | 113 | |
| 117 | // Deliberate, and still to be said while it lasts (`core/capacity.ts`): | |
| 118 | // only an admin sees vitals, so naming what is open here tells nobody else. | |
| 119 | if (egressSetting(input.db) === "report") needs.push(EGRESS_REPORTED); | |
| 120 | ||
| 121 | 114 | return { |
| 122 | 115 | ok: needs.length === 0, |
| 123 | 116 | needs, |
src/web/pages/capacity.ts +4 −5
| @@ -103,13 +103,12 @@ export function capacityPage(ctx: Ctx): Response { | ||
| 103 | 103 | ); |
| 104 | 104 | const body = html`${when( |
| 105 | 105 | reported, |
| 106 | () => html`<p class="error-note mb" id="egress-reported"> | |
| 107 | ${icon("alert")} Egress is reported, not enforced: every new app, | |
| 106 | () => html`<p class="muted small mb" id="egress-reported"> | |
| 107 | ${icon("activity")} Egress is reported, not enforced: every new app, | |
| 108 | 108 | preview, terminal and check cell may reach any public host on ports |
| 109 | 109 | 80 and 443, declared or not. Private, loopback and cluster addresses, |
| 110 | and every other port, stay refused. The forge's vitals (the board's | |
| 111 | strip, the health check for admins) say so while it lasts. Set it back to enforce once each app's reported hosts | |
| 112 | are in its forge.json. | |
| 110 | and every other port, stay refused. Set it back to enforce once each | |
| 111 | app's reported hosts are in its forge.json. | |
| 113 | 112 | </p>`, |
| 114 | 113 | )} |
| 115 | 114 | ${card( |
test/board.test.ts +24
| @@ -11,6 +11,7 @@ import { mkdtemp, rm } from "node:fs/promises"; | ||
| 11 | 11 | import { tmpdir } from "node:os"; |
| 12 | 12 | import { join } from "node:path"; |
| 13 | 13 | import { createApp, type App } from "../src/app.ts"; |
| 14 | import { changeCapacity } from "../src/actions/capacity.ts"; | |
| 14 | 15 | import { enableApp } from "../src/actions/apps.ts"; |
| 15 | 16 | import { addToCrew, createAgent } from "../src/actions/crew.ts"; |
| 16 | 17 | import { openIssue } from "../src/actions/issues.ts"; |
| @@ -455,6 +456,26 @@ describe("the board's forge strip", () => { | ||
| 455 | 456 | expect(other).not.toContain("stuck."); |
| 456 | 457 | }); |
| 457 | 458 | |
| 459 | test("a setting that is not the default is no incident: the strip stays fine and the cards render", async () => { | |
| 460 | const made = await createRepo(app, admin, { name: "calm" }); | |
| 461 | if (!made.ok) throw new Error(String(made.error)); | |
| 462 | const enabled = await enableApp(app, admin, made.value, { slug: "calm" }); | |
| 463 | if (!enabled.ok) throw new Error(String(enabled.error)); | |
| 464 | claimRun(app.db, createRun(app.db, { repoId: made.value.id, kind: "agent" }).id); | |
| 465 | // Clear the stuck run the first test left behind. | |
| 466 | app.db.query("UPDATE run SET state = 'failed' WHERE heartbeat_at = ?1").run("2020-01-01T00:00:00.000Z"); | |
| 467 | expect(changeCapacity(app, admin, "cells.egress", "report").ok).toBe(true); | |
| 468 | try { | |
| 469 | const text = await board("root"); | |
| 470 | expect(text).toContain("All fine"); | |
| 471 | expect(text).not.toContain("board-forge-need"); | |
| 472 | expect(text).not.toContain("error-note"); | |
| 473 | expect(text).toContain("board-card board-run"); | |
| 474 | } finally { | |
| 475 | changeCapacity(app, admin, "cells.egress", "enforce"); | |
| 476 | } | |
| 477 | }); | |
| 478 | ||
| 458 | 479 | test("a blocked rollout shows in the status line, escaped, for an admin only", async () => { |
| 459 | 480 | const guard = mkdtempSync(join(tmpdir(), "forge-board-guard-")); |
| 460 | 481 | process.env["FORGE_GUARD_DIR"] = guard; |
| @@ -467,6 +488,9 @@ describe("the board's forge strip", () => { | ||
| 467 | 488 | const blocked = await board("root"); |
| 468 | 489 | expect(blocked).toContain("rollout blocked: quota refused <i>8Gi</i>"); |
| 469 | 490 | expect(blocked).not.toContain("<i>8Gi</i>"); |
| 491 | // One line, and the board beneath it still renders. | |
| 492 | expect(blocked.match(/class="board-forge-need"/g)?.length).toBe(1); | |
| 493 | expect(blocked).toContain("board-card board-run"); | |
| 470 | 494 | expect(await board("bob")).not.toContain("rollout blocked"); |
| 471 | 495 | } finally { |
| 472 | 496 | delete process.env["FORGE_GUARD_DIR"]; |
test/capacity.test.ts +3 −5
| @@ -35,7 +35,6 @@ import { createUser, type User } from "../src/core/users.ts"; | ||
| 35 | 35 | import { startCrew } from "../src/crew/dispatch.ts"; |
| 36 | 36 | import { Gate } from "../src/git/env.ts"; |
| 37 | 37 | import { readHealth } from "../src/actions/health.ts"; |
| 38 | import { EGRESS_REPORTED } from "../src/kernel/vitals.ts"; | |
| 39 | 38 | import { serve, type Servers } from "../src/http/server.ts"; |
| 40 | 39 | import { readRunLog } from "../src/runs/log.ts"; |
| 41 | 40 | |
| @@ -344,12 +343,12 @@ describe("report mode is loud to admins, and to nobody else", () => { | ||
| 344 | 343 | const needs = (who: User | undefined): string[] => |
| 345 | 344 | "needs" in readHealth(app, who) ? (readHealth(app, who) as { needs: string[] }).needs : []; |
| 346 | 345 | |
| 347 | test("reported: the admin's vitals and the Capacity page name what is open", async () => { | |
| 346 | test("reported: the Capacity page says it once, as a note; vitals do not", async () => { | |
| 348 | 347 | expect(changeCapacity(app, alice, "cells.egress", "report").ok).toBe(true); |
| 349 | expect(needs(alice)).toContain(EGRESS_REPORTED); | |
| 350 | expect(EGRESS_REPORTED).toContain("any public host on ports 80 and 443"); | |
| 348 | expect(needs(alice)).toEqual([]); | |
| 351 | 349 | const shown = await page(); |
| 352 | 350 | expect(shown).toContain('id="egress-reported"'); |
| 351 | expect(shown).not.toMatch(/error-note[^>]*id="egress-reported"/); | |
| 353 | 352 | expect(shown.replace(/\s+/g, " ")).toContain("may reach any public host on ports 80 and 443"); |
| 354 | 353 | }); |
| 355 | 354 | |
| @@ -365,7 +364,6 @@ describe("report mode is loud to admins, and to nobody else", () => { | ||
| 365 | 364 | |
| 366 | 365 | test("enforced: no warning anywhere", async () => { |
| 367 | 366 | expect(changeCapacity(app, alice, "cells.egress", "enforce").ok).toBe(true); |
| 368 | expect(needs(alice)).not.toContain(EGRESS_REPORTED); | |
| 369 | 367 | expect(await page()).not.toContain('id="egress-reported"'); |
| 370 | 368 | }); |
| 371 | 369 | }); |
test/vitals.test.ts +2 −2
| @@ -148,8 +148,8 @@ test("the number of queries does not grow with runs", () => { | ||
| 148 | 148 | if (i % 2) app.db.query("UPDATE run SET state='running' WHERE id=?1").run(r.id); |
| 149 | 149 | } |
| 150 | 150 | expect(count()).toBe(before); |
| 151 | // The queue, the running runs, and the egress mode (`core/capacity.ts`). | |
| 152 | expect(before).toBe(3); | |
| 151 | // The queue and the running runs. | |
| 152 | expect(before).toBe(2); | |
| 153 | 153 | }); |
| 154 | 154 | |
| 155 | 155 | test("errors are counted, bounded, redacted, and a burst is said", async () => { |
test/web.test.ts +14
| @@ -410,6 +410,20 @@ describe("web pages end to end", () => { | ||
| 410 | 410 | expect(stale.status).toBe(404); |
| 411 | 411 | }); |
| 412 | 412 | |
| 413 | test("the home carries no error-note for a setting that is not the default", async () => { | |
| 414 | const { changeCapacity } = await import("../src/actions/capacity.ts"); | |
| 415 | const { getUserByName } = await import("../src/core/users.ts"); | |
| 416 | const ada = getUserByName(app.db, "ada")!; | |
| 417 | expect(changeCapacity(app, ada, "cells.egress", "report").ok).toBe(true); | |
| 418 | try { | |
| 419 | const text = await (await fetch(base, { headers: { cookie } })).text(); | |
| 420 | expect(text).not.toContain("error-note"); | |
| 421 | expect(text).not.toContain("Egress is reported"); | |
| 422 | } finally { | |
| 423 | changeCapacity(app, ada, "cells.egress", "enforce"); | |
| 424 | } | |
| 425 | }); | |
| 426 | ||
| 413 | 427 | test("new repo via the form, empty state renders", async () => { |
| 414 | 428 | const response = await fetch(`${base}/new`, { |
| 415 | 429 | method: "POST", |
new above
offline
Good: a muted note on the page that owns the setting. Check at 360px that it stays readable and does not push the controls below the fold.