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

Merged opened this

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

Merged

steward/front-page → main 3 commits

  • Merged. steward/front-page was merged into main as 5ebaeab.

Review

Sign in to review this change.
4 files changed +62 −10
scripts/gate.ts +3 −6
@@ -104,6 +104,9 @@ export const GATE: readonly string[] = [
104104 "test/live.test.ts",
105105 "test/map.test.ts",
106106 "test/mcp.test.ts",
107 "test/looks.test.ts",
108 "test/branch_tell.test.ts",
109 "test/forge-cloudflare-celld.test.ts",
107110 "test/notify.test.ts",
108111 "test/onboarding.test.ts",
109112 "test/origins-oauth.test.ts",
@@ -163,12 +166,6 @@ export const EXCLUDED: readonly Exclusion[] = [
163166 reason: "red: wall-clock bounds (5 s, 200 ms) fail on a loaded machine",
164167 issue: 584,
165168 },
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 },
172169 {
173170 file: "test/origins-security-provider.test.ts",
174171 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 {
187187 readonly env: EnvName;
188188 readonly commitOid: string;
189189 readonly at: number;
190 /** Queued by the sweep: a picture that failed at this head is not retaken. */
191 readonly swept?: true;
190192 }
191193
192194 export function startLooks(app: App, options: LooksOptions): LooksHandle {
@@ -331,7 +333,13 @@ export function startLooks(app: App, options: LooksOptions): LooksHandle {
331333 const status = runtime.envStatus(record, item.env);
332334 if (status?.state !== "running" || status.commitOid !== item.commitOid)
333335 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 );
335343 if (wanted.length === 0) return;
336344 // The phone and desktop pictures are what the owner sees: the app
337345 // 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 {
460468 // deploy tries again, not every sweep.
461469 const { commitOid, looks: pages } = status;
462470 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 });
464478 }
465479 } catch (error) {
466480 log.error("looks sweep failed", { error: String(error) });
src/web/pages/looks.ts +13 −2
@@ -21,7 +21,7 @@ import {
2121 type LookKind,
2222 } from "../../core/looks.ts";
2323 import type { Pull } from "../../core/pulls.ts";
24 import type { Repo } from "../../core/repos.ts";
24 import { canSeeOwnerFacts, type Repo } from "../../core/repos.ts";
2525 import type { Ctx } from "../../http/context.ts";
2626 import { notFound } from "../../http/respond.ts";
2727 import { loadRepo, repoBase } from "../chrome.ts";
@@ -72,6 +72,10 @@ export function lookImage(ctx: Ctx): Response {
7272 (l) => l.commitOid === commit && lookSlot(l.page, l.kind) === slot,
7373 );
7474 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();
7579 const path = join(ctx.app.config.dataDir, look.path);
7680 if (!existsSync(path)) return notFound();
7781 return new Response(Bun.file(path), {
@@ -98,6 +102,8 @@ export function lookCard(
98102 pull: Pull,
99103 headOid: string,
100104 ): 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``;
101107 const base = repoBase(repo);
102108 const env = previewEnv(pull.number);
103109 const looks = looksForEnv(ctx.app.db, record.id, env);
@@ -188,7 +194,12 @@ export function lookNow(ctx: Ctx, repo: Repo, record: AppRecord): Html {
188194 const looks = looksForEnv(ctx.app.db, record.id, "production").filter(
189195 (l) => l.page === DEFAULT_PAGE,
190196 );
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);
192203 if (look === undefined) return html``;
193204 const base = repoBase(repo);
194205 const size = LOOK_SIZES[look.kind];
test/looks.test.ts +30
@@ -713,6 +713,36 @@ describe("the look", () => {
713713 expect(calls).toHaveLength(before + 1);
714714 }, 60_000);
715715
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
716746 test("memory pressure is what the container truly holds, never its page cache", async () => {
717747 const { cgroupMemoryPressure, memoryPressure } =
718748 await import("../src/looks/service.ts");

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs