Public: anyone can read this; the crew can change it.
The studio front door loads a full-size screenshot per app tile where a 200x125 thumb already exists #320
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/287-the-studio-front-door-loads-a-full-size → main 1 commit fork
3 files
changed +113
−10
src/web/pages/looks.ts +4 −2
| @@ -40,8 +40,10 @@ export const lookUrl = ( | ||
| 40 | 40 | ): string => `${base}/looks/${env}/${commitOid}/${lookSlot(page, kind)}.png`; |
| 41 | 41 | |
| 42 | 42 | /** The newest picture at one width, when there is one. */ |
| 43 | const pictured = (looks: readonly Look[], kind: LookKind): Look | undefined => | |
| 44 | looks.find((l) => l.kind === kind && l.error === null); | |
| 43 | export const pictured = ( | |
| 44 | looks: readonly Look[], | |
| 45 | kind: LookKind, | |
| 46 | ): Look | undefined => looks.find((l) => l.kind === kind && l.error === null); | |
| 45 | 47 | |
| 46 | 48 | /** |
| 47 | 49 | * The picture for a small slot: the dedicated thumb, or — for a commit |
src/web/pages/site.ts +7 −8
| @@ -52,7 +52,7 @@ import { | ||
| 52 | 52 | import { field, readForm } from "../form.ts"; |
| 53 | 53 | import { html, when, type Html } from "../html.ts"; |
| 54 | 54 | import { icon, type IconName } from "../icons.ts"; |
| 55 | import { lookUrl } from "./looks.ts"; | |
| 55 | import { lookUrl, pictured } from "./looks.ts"; | |
| 56 | 56 | import { |
| 57 | 57 | eventActor, |
| 58 | 58 | eventVerb, |
| @@ -553,22 +553,21 @@ function studioLanding(ctx: Ctx): Html { | ||
| 553 | 553 | const production = ( |
| 554 | 554 | runtime === undefined ? [] : runtime.status(record) |
| 555 | 555 | ).find((env) => env.env === "production"); |
| 556 | const look = looks.find( | |
| 556 | const ofApp = looks.filter( | |
| 557 | 557 | (l) => |
| 558 | 558 | l.appId === record.id && |
| 559 | 559 | l.env === "production" && |
| 560 | l.page === DEFAULT_PAGE && | |
| 561 | l.kind === "desktop" && | |
| 562 | l.error === null, | |
| 560 | l.page === DEFAULT_PAGE, | |
| 563 | 561 | ); |
| 562 | const look = pictured(ofApp, "thumb") ?? pictured(ofApp, "desktop"); | |
| 564 | 563 | const base = repoBase(repo); |
| 565 | 564 | return html`<a class="app-tile" href="${base}"> |
| 566 | 565 | ${look |
| 567 | 566 | ? html`<img |
| 568 | 567 | class="app-tile-look" |
| 569 | src="${lookUrl(base, "production", look.commitOid, DEFAULT_PAGE, "desktop")}" | |
| 570 | width="${LOOK_SIZES.desktop.width}" | |
| 571 | height="${LOOK_SIZES.desktop.height}" | |
| 568 | src="${lookUrl(base, "production", look.commitOid, DEFAULT_PAGE, look.kind)}" | |
| 569 | width="${LOOK_SIZES[look.kind].width}" | |
| 570 | height="${LOOK_SIZES[look.kind].height}" | |
| 572 | 571 | alt="" |
| 573 | 572 | loading="lazy" |
| 574 | 573 | />` |
test/studio.test.ts +102
| @@ -1535,6 +1535,108 @@ describe("the studio", () => { | ||
| 1535 | 1535 | expect(dev).toContain("Your code, your machine"); |
| 1536 | 1536 | }); |
| 1537 | 1537 | |
| 1538 | test("the front door's tile is the thumb, not the full desktop picture, when both exist (#287)", async () => { | |
| 1539 | const created = await createRepo(app, alice, { | |
| 1540 | name: "tinytile", | |
| 1541 | visibility: "public", | |
| 1542 | description: "picked its own thumb", | |
| 1543 | }); | |
| 1544 | if (!created.ok) throw new Error(String(created.error)); | |
| 1545 | const seeded = await commitToBranch(app, alice, created.value, { | |
| 1546 | message: "Start", | |
| 1547 | files: [ | |
| 1548 | { | |
| 1549 | path: "package.json", | |
| 1550 | content: '{"name":"tinytile","scripts":{"start":"bun run server.ts"}}\n', | |
| 1551 | }, | |
| 1552 | { | |
| 1553 | path: "server.ts", | |
| 1554 | content: | |
| 1555 | "Bun.serve({ port: Number(process.env.PORT), fetch: () => new Response('ok') });\n", | |
| 1556 | }, | |
| 1557 | ], | |
| 1558 | }); | |
| 1559 | if (!seeded.ok) throw new Error(JSON.stringify(seeded.error)); | |
| 1560 | const enabled = await enableApp(app, alice, created.value, { | |
| 1561 | slug: "tinytile", | |
| 1562 | }); | |
| 1563 | if (!enabled.ok) throw new Error(String(enabled.error)); | |
| 1564 | const record = getAppByRepo(app.db, created.value.id)!; | |
| 1565 | const commit = seeded.value.commitOid; | |
| 1566 | const drop = (kind: "desktop" | "thumb") => { | |
| 1567 | const rel = join("looks", record.id, `production-${kind}.png`); | |
| 1568 | const abs = join(app.config.dataDir, rel); | |
| 1569 | mkdirSync(dirname(abs), { recursive: true }); | |
| 1570 | writeFileSync(abs, PNG); | |
| 1571 | recordLook(app.db, { appId: record.id, env: "production", commitOid: commit, kind, path: rel }); | |
| 1572 | }; | |
| 1573 | drop("desktop"); | |
| 1574 | drop("thumb"); | |
| 1575 | ||
| 1576 | const anon = await ( | |
| 1577 | await fetch(`${base}/`, { headers: { cookie: "forge_mode=studio" } }) | |
| 1578 | ).text(); | |
| 1579 | const tile = anon | |
| 1580 | .split("<img") | |
| 1581 | .find((chunk) => chunk.includes(`/${created.value.ownerName}/${created.value.name}/looks/`)); | |
| 1582 | expect(tile).toBeDefined(); | |
| 1583 | expect(tile).toContain(`/looks/production/${commit}/thumb.png`); | |
| 1584 | expect(tile).toContain('width="200"'); | |
| 1585 | expect(tile).toContain('height="125"'); | |
| 1586 | }); | |
| 1587 | ||
| 1588 | test("the front door's tile still renders at desktop's size for a commit from before thumbs existed (#287)", async () => { | |
| 1589 | const created = await createRepo(app, alice, { | |
| 1590 | name: "oldtile", | |
| 1591 | visibility: "public", | |
| 1592 | description: "no thumb yet", | |
| 1593 | }); | |
| 1594 | if (!created.ok) throw new Error(String(created.error)); | |
| 1595 | const seeded = await commitToBranch(app, alice, created.value, { | |
| 1596 | message: "Start", | |
| 1597 | files: [ | |
| 1598 | { | |
| 1599 | path: "package.json", | |
| 1600 | content: '{"name":"oldtile","scripts":{"start":"bun run server.ts"}}\n', | |
| 1601 | }, | |
| 1602 | { | |
| 1603 | path: "server.ts", | |
| 1604 | content: | |
| 1605 | "Bun.serve({ port: Number(process.env.PORT), fetch: () => new Response('ok') });\n", | |
| 1606 | }, | |
| 1607 | ], | |
| 1608 | }); | |
| 1609 | if (!seeded.ok) throw new Error(JSON.stringify(seeded.error)); | |
| 1610 | const enabled = await enableApp(app, alice, created.value, { | |
| 1611 | slug: "oldtile", | |
| 1612 | }); | |
| 1613 | if (!enabled.ok) throw new Error(String(enabled.error)); | |
| 1614 | const record = getAppByRepo(app.db, created.value.id)!; | |
| 1615 | const commit = seeded.value.commitOid; | |
| 1616 | const rel = join("looks", record.id, "production-desktop.png"); | |
| 1617 | const abs = join(app.config.dataDir, rel); | |
| 1618 | mkdirSync(dirname(abs), { recursive: true }); | |
| 1619 | writeFileSync(abs, PNG); | |
| 1620 | recordLook(app.db, { | |
| 1621 | appId: record.id, | |
| 1622 | env: "production", | |
| 1623 | commitOid: commit, | |
| 1624 | kind: "desktop", | |
| 1625 | path: rel, | |
| 1626 | }); | |
| 1627 | ||
| 1628 | const anon = await ( | |
| 1629 | await fetch(`${base}/`, { headers: { cookie: "forge_mode=studio" } }) | |
| 1630 | ).text(); | |
| 1631 | const tile = anon | |
| 1632 | .split("<img") | |
| 1633 | .find((chunk) => chunk.includes(`/${created.value.ownerName}/${created.value.name}/looks/`)); | |
| 1634 | expect(tile).toBeDefined(); | |
| 1635 | expect(tile).toContain(`/looks/production/${commit}/desktop.png`); | |
| 1636 | expect(tile).toContain('width="1280"'); | |
| 1637 | expect(tile).toContain('height="800"'); | |
| 1638 | }); | |
| 1639 | ||
| 1538 | 1640 | test("the front door leads with apps the viewer can write, never a stranger's public one — but /apps still lists it, owner named", async () => { |
| 1539 | 1641 | // A public app of alice's own, distinct from "notes" (private) and |
| 1540 | 1642 | // "beacon" (made public by an earlier test) — self-contained so this |
new above
offline