Public: anyone can read this; the crew can change it.
The /__forge/ door says who of the crew is working on this app or preview now, as the pill does #7857
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/7853-the-forge-door-says-who-of-the-crew-is-w → main 1 commit fork
3 files
changed +60
−2
src/apps/door.ts +10 −1
| @@ -7,6 +7,7 @@ | ||
| 7 | 7 | import { createHash } from "node:crypto"; |
| 8 | 8 | import { escapeAttr, type PillWhere } from "./toolbar.ts"; |
| 9 | 9 | import type { Served, Serving } from "./serving.ts"; |
| 10 | import type { CrewNow } from "./crew.ts"; | |
| 10 | 11 | |
| 11 | 12 | export type DoorFacts = { |
| 12 | 13 | where: PillWhere; |
| @@ -24,6 +25,8 @@ export type DoorFacts = { | ||
| 24 | 25 | ssh?: string | null; |
| 25 | 26 | /** Open pulls a viewer may see, each with where to go: its preview or the forge. */ |
| 26 | 27 | pulls?: DoorPullLine[] | undefined; |
| 28 | /** Who of the crew is working here now, as the pill's crew line says it. */ | |
| 29 | crew?: CrewNow | undefined; | |
| 27 | 30 | }; |
| 28 | 31 | |
| 29 | 32 | /** How many pulls the door lists; one more read than this means there are others. */ |
| @@ -108,6 +111,12 @@ function pullsHtml(pulls: DoorPullLine[] | undefined, morePulls: string | undefi | ||
| 108 | 111 | return `<h2>Open pulls</h2><ul>${items.join("")}</ul>${more}`; |
| 109 | 112 | } |
| 110 | 113 | |
| 114 | /** The pill's crew line: who is working on this, linked to that run on the forge. */ | |
| 115 | function crewHtml(crew: CrewNow | undefined): string { | |
| 116 | if (crew === undefined) return ""; | |
| 117 | return `<p><a href="${escapeAttr(crew.url)}">${escapeAttr(`${crew.who} is working on this`)}</a></p>`; | |
| 118 | } | |
| 119 | ||
| 111 | 120 | export function doorPage(facts: DoorFacts, head: boolean): Response { |
| 112 | 121 | const { where, forgeUrl } = facts; |
| 113 | 122 | const links = [link(forgeUrl, "Home")]; |
| @@ -129,7 +138,7 @@ export function doorPage(facts: DoorFacts, head: boolean): Response { | ||
| 129 | 138 | : `<p>Terminal:</p><pre><code>${escapeAttr(facts.ssh)}</code></pre>`; |
| 130 | 139 | const html = `<!doctype html> |
| 131 | 140 | <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex"><title>${escapeAttr(where.app)}, ${escapeAttr(doorWhere(where))}</title><style>${STYLE}</style></head> |
| 132 | <body><main><h1>${escapeAttr(where.app)}</h1><p>${whereLine(where, forgeUrl)}</p>${servingHtml(facts)}${pullsHtml(facts.pulls, morePulls)}<ul>${links.join("")}</ul>${terminal}</main></body></html> | |
| 141 | <body><main><h1>${escapeAttr(where.app)}</h1><p>${whereLine(where, forgeUrl)}</p>${crewHtml(facts.crew)}${servingHtml(facts)}${pullsHtml(facts.pulls, morePulls)}<ul>${links.join("")}</ul>${terminal}</main></body></html> | |
| 133 | 142 | `; |
| 134 | 143 | return new Response(head ? null : html, { |
| 135 | 144 | headers: { |
src/apps/proxy.ts +9
| @@ -910,11 +910,20 @@ export async function proxyApp( | ||
| 910 | 910 | const pulls = timePhase(request, "pulls", () => |
| 911 | 911 | doorPulls(runtime, target, signed?.who), |
| 912 | 912 | ); |
| 913 | const crew = crewNow( | |
| 914 | runtime.app.db, | |
| 915 | runtime.options.forgeUrl, | |
| 916 | target.app, | |
| 917 | target.env, | |
| 918 | signed?.who?.id, | |
| 919 | request, | |
| 920 | ); | |
| 913 | 921 | const page = timePhase(request, "render", () => |
| 914 | 922 | doorPage( |
| 915 | 923 | { |
| 916 | 924 | where, |
| 917 | 925 | pulls, |
| 926 | ...(crew === undefined ? {} : { crew }), | |
| 918 | 927 | ...(serving === undefined ? {} : { serving }), |
| 919 | 928 | forgeUrl: runtime.options.forgeUrl, |
| 920 | 929 | ...(ssh === undefined ? {} : { ssh }), |
test/apps-door.test.ts +41 −1
| @@ -12,6 +12,8 @@ import { signGrant, type AppTarget } from "../src/apps/proxy.ts"; | ||
| 12 | 12 | import { AppsRuntime } from "../src/apps/runtime.ts"; |
| 13 | 13 | import { getAppByRepo, type AppRecord } from "../src/core/apps.ts"; |
| 14 | 14 | import { createIssue } from "../src/core/issues.ts"; |
| 15 | import { claimRun, createRun, finishRun } from "../src/core/runs.ts"; | |
| 16 | import { setSealed } from "../src/core/sealed.ts"; | |
| 15 | 17 | import { createUser, getUserById } from "../src/core/users.ts"; |
| 16 | 18 | import { serve, type Servers } from "../src/http/server.ts"; |
| 17 | 19 | import { quietLogs } from "./helpers/globals.ts"; |
| @@ -319,7 +321,7 @@ test("the door and the crew line name the phases they read, signed in, on produc | ||
| 319 | 321 | [`notes--${pull}.${DOMAIN}`, grantFor(`pull-${pull}`)], |
| 320 | 322 | ]; |
| 321 | 323 | const reads: Record<string, string[]> = { |
| 322 | "/__forge/": ["session", "app", "pull", "serving", "pulls", "render"], | |
| 324 | "/__forge/": ["session", "app", "pull", "serving", "pulls", "working", "render"], | |
| 323 | 325 | "/__forge/crew": ["session", "app", "pull", "working", "side", "render"], |
| 324 | 326 | }; |
| 325 | 327 | for (const [host, cookie] of hosts) |
| @@ -429,3 +431,41 @@ test("the door links a dormant preview to itself, and says so when more than fiv | ||
| 429 | 431 | expect(html).toContain("More pulls are open on the forge."); |
| 430 | 432 | expect((html.match(/ · check /g) ?? []).length).toBe(5); |
| 431 | 433 | }); |
| 434 | ||
| 435 | test("the door names the crew run working here, linked, to who may see it", async () => { | |
| 436 | const bob = await createUser(app.db, { | |
| 437 | origin: "seed", | |
| 438 | trusted: true, | |
| 439 | username: "scoutbot", | |
| 440 | email: "[email protected]", | |
| 441 | password: "a-good-passphrase", | |
| 442 | }); | |
| 443 | if (!bob.ok) throw new Error(bob.error); | |
| 444 | const repoId = record.repoId; | |
| 445 | const open = createIssue(app.db, { repoId, authorId: alice, title: "o", kind: "pull" }); | |
| 446 | const sealed = createIssue(app.db, { repoId, authorId: alice, title: "s", kind: "issue" }); | |
| 447 | if (!open.ok || !sealed.ok) throw new Error("issue"); | |
| 448 | setSealed(app.db, sealed.value.id, true); | |
| 449 | const target = { app: record, env: "production" } as AppTarget; | |
| 450 | const cookie = `forge_grant_${record.id.slice(-8)}=${signGrant(dataDir, target, 60_000, Date.now(), { id: alice, name: "alice", role: "admin" })}`; | |
| 451 | const start = (issueId: string) => { | |
| 452 | const run = createRun(app.db, { repoId, kind: "agent", issueId, actorId: bob.value.id, reason: "assigned" }); | |
| 453 | claimRun(app.db, run.id); | |
| 454 | return run; | |
| 455 | }; | |
| 456 | const idle = await (await door(`notes.${DOMAIN}`, cookie)).text(); | |
| 457 | expect(idle).not.toContain("is working on this"); | |
| 458 | const hidden = start(sealed.value.id); | |
| 459 | // A signed-in reader who may not see the sealed issue is not told of its run. | |
| 460 | const other = signGrant(dataDir, target, 60_000, Date.now(), { id: bob.value.id, name: "scoutbot", role: "user" }); | |
| 461 | const otherCookie = `forge_grant_${record.id.slice(-8)}=${other}`; | |
| 462 | expect(await (await door(`notes.${DOMAIN}`, otherCookie)).text()).not.toContain("is working on this"); | |
| 463 | expect(await (await door(`notes.${DOMAIN}`)).text()).not.toContain("is working on this"); | |
| 464 | const run = start(open.value.id); | |
| 465 | const html = await (await door(`notes.${DOMAIN}`, cookie)).text(); | |
| 466 | expect(html).toContain(`<a href="${servers.url}/alice/notes/runs/${run.id}">scoutbot is working on this</a>`); | |
| 467 | expect(html).not.toContain("<script"); | |
| 468 | finishRun(app.db, run.id, { state: "ok", summary: "d" }); | |
| 469 | finishRun(app.db, hidden.id, { state: "ok", summary: "d" }); | |
| 470 | expect(await (await door(`notes.${DOMAIN}`, cookie)).text()).not.toContain("is working on this"); | |
| 471 | }); |
new above
offline