Public: anyone can read this; the crew can change it.
Front-page thumbs are the app as a stranger sees it, reduced, not a corner taken in a tile-sized window #684
Check · Review · Land · Live done — Landed and live in production.
steward/front-page → main 3 commits
4 files
changed +62
−10
scripts/gate.ts +3 −6
| @@ -104,6 +104,9 @@ export const GATE: readonly string[] = [ | ||
| 104 | 104 | "test/live.test.ts", |
| 105 | 105 | "test/map.test.ts", |
| 106 | 106 | "test/mcp.test.ts", |
| 107 | "test/looks.test.ts", | |
| 108 | "test/branch_tell.test.ts", | |
| 109 | "test/forge-cloudflare-celld.test.ts", | |
| 107 | 110 | "test/notify.test.ts", |
| 108 | 111 | "test/onboarding.test.ts", |
| 109 | 112 | "test/origins-oauth.test.ts", |
| @@ -163,12 +166,6 @@ export const EXCLUDED: readonly Exclusion[] = [ | ||
| 163 | 166 | reason: "red: wall-clock bounds (5 s, 200 ms) fail on a loaded machine", |
| 164 | 167 | issue: 584, |
| 165 | 168 | }, |
| 166 | { | |
| 167 | file: "test/looks.test.ts", | |
| 168 | reason: | |
| 169 | "red: asserts the dashboard and studio pages as they were before the redesign", | |
| 170 | issue: 291, | |
| 171 | }, | |
| 172 | 169 | { |
| 173 | 170 | file: "test/origins-security-provider.test.ts", |
| 174 | 171 | reason: "red: the off-host redirect test cannot connect to its own localhost origin", |
src/looks/service.ts +16 −2
| @@ -187,6 +187,8 @@ interface Pending { | ||
| 187 | 187 | readonly env: EnvName; |
| 188 | 188 | readonly commitOid: string; |
| 189 | 189 | readonly at: number; |
| 190 | /** Queued by the sweep: a picture that failed at this head is not retaken. */ | |
| 191 | readonly swept?: true; | |
| 190 | 192 | } |
| 191 | 193 | |
| 192 | 194 | export function startLooks(app: App, options: LooksOptions): LooksHandle { |
| @@ -331,7 +333,13 @@ export function startLooks(app: App, options: LooksOptions): LooksHandle { | ||
| 331 | 333 | const status = runtime.envStatus(record, item.env); |
| 332 | 334 | if (status?.state !== "running" || status.commitOid !== item.commitOid) |
| 333 | 335 | return; |
| 334 | const wanted = missing(item.appId, item.env, item.commitOid, status.looks); | |
| 336 | const wanted = missing( | |
| 337 | item.appId, | |
| 338 | item.env, | |
| 339 | item.commitOid, | |
| 340 | status.looks, | |
| 341 | item.swept === true, | |
| 342 | ); | |
| 335 | 343 | if (wanted.length === 0) return; |
| 336 | 344 | // The phone and desktop pictures are what the owner sees: the app |
| 337 | 345 | // renders for a signed-in admin, exactly as when they open it from the |
| @@ -460,7 +468,13 @@ export function startLooks(app: App, options: LooksOptions): LooksHandle { | ||
| 460 | 468 | // deploy tries again, not every sweep. |
| 461 | 469 | const { commitOid, looks: pages } = status; |
| 462 | 470 | if (missing(record.id, "production", commitOid, pages, true).length > 0) |
| 463 | enqueue({ appId: record.id, env: "production", commitOid, at: Date.now() }); | |
| 471 | enqueue({ | |
| 472 | appId: record.id, | |
| 473 | env: "production", | |
| 474 | commitOid, | |
| 475 | at: Date.now(), | |
| 476 | swept: true, | |
| 477 | }); | |
| 464 | 478 | } |
| 465 | 479 | } catch (error) { |
| 466 | 480 | log.error("looks sweep failed", { error: String(error) }); |
src/web/pages/looks.ts +13 −2
| @@ -21,7 +21,7 @@ import { | ||
| 21 | 21 | type LookKind, |
| 22 | 22 | } from "../../core/looks.ts"; |
| 23 | 23 | import type { Pull } from "../../core/pulls.ts"; |
| 24 | import type { Repo } from "../../core/repos.ts"; | |
| 24 | import { canSeeOwnerFacts, type Repo } from "../../core/repos.ts"; | |
| 25 | 25 | import type { Ctx } from "../../http/context.ts"; |
| 26 | 26 | import { notFound } from "../../http/respond.ts"; |
| 27 | 27 | import { loadRepo, repoBase } from "../chrome.ts"; |
| @@ -72,6 +72,10 @@ export function lookImage(ctx: Ctx): Response { | ||
| 72 | 72 | (l) => l.commitOid === commit && lookSlot(l.page, l.kind) === slot, |
| 73 | 73 | ); |
| 74 | 74 | if (look === undefined || look.error !== null) return notFound(); |
| 75 | // Phone and desktop are taken as the owner, signed in as an admin: what | |
| 76 | // they show is the writers' to see. The thumb is taken as a stranger. | |
| 77 | if (look.kind !== "thumb" && !canSeeOwnerFacts(ctx.app.db, repo, ctx.auth.user)) | |
| 78 | return notFound(); | |
| 75 | 79 | const path = join(ctx.app.config.dataDir, look.path); |
| 76 | 80 | if (!existsSync(path)) return notFound(); |
| 77 | 81 | return new Response(Bun.file(path), { |
| @@ -98,6 +102,8 @@ export function lookCard( | ||
| 98 | 102 | pull: Pull, |
| 99 | 103 | headOid: string, |
| 100 | 104 | ): Html { |
| 105 | // Its pictures are the owner's view: a reader who cannot write gets none. | |
| 106 | if (!canSeeOwnerFacts(ctx.app.db, repo, ctx.auth.user)) return html``; | |
| 101 | 107 | const base = repoBase(repo); |
| 102 | 108 | const env = previewEnv(pull.number); |
| 103 | 109 | const looks = looksForEnv(ctx.app.db, record.id, env); |
| @@ -188,7 +194,12 @@ export function lookNow(ctx: Ctx, repo: Repo, record: AppRecord): Html { | ||
| 188 | 194 | const looks = looksForEnv(ctx.app.db, record.id, "production").filter( |
| 189 | 195 | (l) => l.page === DEFAULT_PAGE, |
| 190 | 196 | ); |
| 191 | const look = pictured(looks, "thumb") ?? pictured(looks, "desktop"); | |
| 197 | // The desktop picture is the owner's view: only a writer falls back to it. | |
| 198 | const look = | |
| 199 | pictured(looks, "thumb") ?? | |
| 200 | (canSeeOwnerFacts(ctx.app.db, repo, ctx.auth.user) | |
| 201 | ? pictured(looks, "desktop") | |
| 202 | : undefined); | |
| 192 | 203 | if (look === undefined) return html``; |
| 193 | 204 | const base = repoBase(repo); |
| 194 | 205 | const size = LOOK_SIZES[look.kind]; |
test/looks.test.ts +30
| @@ -713,6 +713,36 @@ describe("the look", () => { | ||
| 713 | 713 | expect(calls).toHaveLength(before + 1); |
| 714 | 714 | }, 60_000); |
| 715 | 715 | |
| 716 | test("a public app's owner-view pictures are its writers' alone: a stranger and a reader get the thumb, never phone or desktop", async () => { | |
| 717 | app.db.query(`UPDATE repo SET visibility = 'public' WHERE id = ?1`).run(repo.id); | |
| 718 | try { | |
| 719 | const head = runtime.envStatus(record, "production")!.commitOid; | |
| 720 | // The test before left this head's thumb failed; picture it again. | |
| 721 | const thumbPath = `looks/${record.id}/production/${head}-thumb.png`; | |
| 722 | writeFileSync(join(dataDir, thumbPath), PNG); | |
| 723 | recordLook(app.db, { | |
| 724 | appId: record.id, | |
| 725 | env: "production", | |
| 726 | commitOid: head, | |
| 727 | kind: "thumb", | |
| 728 | path: thumbPath, | |
| 729 | }); | |
| 730 | const at = (kind: string) => `/alice/hello/looks/production/${head}/${kind}.png`; | |
| 731 | for (const cookie of ["", bobCookie]) { | |
| 732 | expect((await web(at("desktop"), cookie)).status).toBe(404); | |
| 733 | expect((await web(at("phone"), cookie)).status).toBe(404); | |
| 734 | expect((await web(at("thumb"), cookie)).status).toBe(200); | |
| 735 | } | |
| 736 | expect((await web(at("desktop"), aliceCookie)).status).toBe(200); | |
| 737 | // The app's page shows a reader the thumb, never the owner's desktop. | |
| 738 | const page = await (await web("/alice/hello", `${bobCookie}; forge_mode=studio`)).text(); | |
| 739 | expect(page).not.toContain("/desktop.png"); | |
| 740 | expect(page).not.toContain("/phone.png"); | |
| 741 | } finally { | |
| 742 | app.db.query(`UPDATE repo SET visibility = 'private' WHERE id = ?1`).run(repo.id); | |
| 743 | } | |
| 744 | }); | |
| 745 | ||
| 716 | 746 | test("memory pressure is what the container truly holds, never its page cache", async () => { |
| 717 | 747 | const { cgroupMemoryPressure, memoryPressure } = |
| 718 | 748 | await import("../src/looks/service.ts"); |
new above
offline