From 4914018be91d31e4e4040a763a48ebfa83843909 Mon Sep 17 00:00:00 2001 From: Robin-ELM <64148042+Robin-ELM@users.noreply.github.com> Date: Wed, 4 Nov 2026 08:38:05 +0000 Subject: [PATCH 1/5] Promote dev to prod (6048800) --- .env.example | 3 + .gitignore | 1 + apphosting.dev.yaml | 7 + apphosting.prod.yaml | 5 + docs/14-design-system.md | 2 +- docs/17-showcase-sets.md | 198 +++++++++++++++--- docs/DEPLOY.md | 7 +- eval/showcase/README.md | 2 +- package.json | 1 + scripts/showcase-build.mjs | 239 ++++++++++++++++++++++ scripts/showcase/driver.mjs | 309 +++++++++++++++++++++++++++++ scripts/showcase/driver.test.mjs | 225 +++++++++++++++++++++ scripts/showcase/photos.mjs | 141 +++++++++++++ scripts/showcase/photos.test.mjs | 168 ++++++++++++++++ scripts/showcase/serve.mjs | 63 ++++++ scripts/showcase/serve.test.mjs | 71 +++++++ scripts/showcase/sheet.mjs | 98 +++++++++ scripts/showcase/sheet.test.mjs | 88 ++++++++ scripts/showcase/spec.mjs | 215 ++++++++++++++++++++ scripts/showcase/spec.test.mjs | 210 ++++++++++++++++++++ scripts/showcase/test-support.mjs | 143 +++++++++++++ scripts/showcase/web.mjs | 171 ++++++++++++++++ scripts/showcase/web.test.mjs | 211 ++++++++++++++++++++ src/app/_legal/LegalParts.tsx | 36 +++- src/app/g/[slug]/board.module.css | 2 + src/app/landing-page.test.ts | 104 +++++++++- src/app/new/start-flow.css | 2 +- src/app/page.tsx | 60 +++++- src/config.test.ts | 12 ++ src/config.ts | 26 +++ src/domain/landing-actions.test.ts | 19 ++ src/domain/landing-actions.ts | 29 +++ src/i18n/messages/de.json | 9 +- src/i18n/messages/de.lock.json | 9 +- src/i18n/messages/en.json | 9 +- src/i18n/messages/es.json | 9 +- src/i18n/messages/es.lock.json | 9 +- src/i18n/messages/fr.json | 9 +- src/i18n/messages/fr.lock.json | 9 +- src/i18n/messages/id.json | 9 +- src/i18n/messages/id.lock.json | 9 +- src/i18n/messages/it.json | 9 +- src/i18n/messages/it.lock.json | 9 +- src/i18n/messages/pt-BR.json | 9 +- src/i18n/messages/pt-BR.lock.json | 9 +- src/server/landing-viewer.ts | 34 ++++ src/server/my-boards.test.ts | 43 +++- src/server/my-boards.ts | 26 +++ src/ui/icons.tsx | 3 + src/ui/kit.test.ts | 6 +- src/ui/pinneds.css | 37 +++- vitest.config.ts | 2 +- 52 files changed, 3049 insertions(+), 87 deletions(-) create mode 100644 scripts/showcase-build.mjs create mode 100644 scripts/showcase/driver.mjs create mode 100644 scripts/showcase/driver.test.mjs create mode 100644 scripts/showcase/photos.mjs create mode 100644 scripts/showcase/photos.test.mjs create mode 100644 scripts/showcase/serve.mjs create mode 100644 scripts/showcase/serve.test.mjs create mode 100644 scripts/showcase/sheet.mjs create mode 100644 scripts/showcase/sheet.test.mjs create mode 100644 scripts/showcase/spec.mjs create mode 100644 scripts/showcase/spec.test.mjs create mode 100644 scripts/showcase/test-support.mjs create mode 100644 scripts/showcase/web.mjs create mode 100644 scripts/showcase/web.test.mjs create mode 100644 src/domain/landing-actions.test.ts create mode 100644 src/domain/landing-actions.ts create mode 100644 src/server/landing-viewer.ts diff --git a/.env.example b/.env.example index cfa4f8e4..0ea8a244 100644 --- a/.env.example +++ b/.env.example @@ -22,6 +22,9 @@ DEMO_BOARD_SLUG= # everyone; set it in the host env, never commit a value here. Unset = # the pages link to GitHub issues for non-private questions. LEGAL_CONTACT_EMAIL= +# PIN-263: judging-week landing links (https only; unset = hidden) +DEMO_VIDEO_URL= +PROMO_POST_URL= # Board Plus billing (PIN-103/PIN-149, docs/15-board-plus.md): off (default) # answers every checkout "unavailable" and writes nothing; mock grants # Plus instantly for demos (labelled source "mock", no money moves). diff --git a/.gitignore b/.gitignore index 178706cd..b9d9e092 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ coverage/ .DS_Store .spike-work/ +.showcase-work/ diff --git a/apphosting.dev.yaml b/apphosting.dev.yaml index 29b0359a..c03137b4 100644 --- a/apphosting.dev.yaml +++ b/apphosting.dev.yaml @@ -30,6 +30,13 @@ env: # address is printed on both pages, so it is not a secret. - variable: LEGAL_CONTACT_EMAIL value: contact@moriacrew.com + # PIN-263: judging-week links on the landing, public plain values. + # Unset a value to take its link down (no code change). + - variable: DEMO_VIDEO_URL + value: https://youtu.be/q6lkrA6fsCI + # A test post until the launch post is published. + - variable: PROMO_POST_URL + value: https://x.com/MoriaCrew/status/2099512136368071033 - variable: DATA_BACKEND value: firestore - variable: FIREBASE_PROJECT_ID diff --git a/apphosting.prod.yaml b/apphosting.prod.yaml index 4edeef53..ef005df9 100644 --- a/apphosting.prod.yaml +++ b/apphosting.prod.yaml @@ -52,6 +52,11 @@ env: # address is printed on both pages, so it is not a secret. - variable: LEGAL_CONTACT_EMAIL value: contact@moriacrew.com + # PIN-263: judging-week links on the landing, public plain values. + # Unset a value to take its link down (no code change). + - variable: DEMO_VIDEO_URL + value: https://youtu.be/q6lkrA6fsCI + # PROMO_POST_URL: add the launch post's link here once it is published. - variable: DATA_BACKEND value: firestore - variable: FIREBASE_PROJECT_ID diff --git a/docs/14-design-system.md b/docs/14-design-system.md index e7dc98e4..66ffa7e9 100644 --- a/docs/14-design-system.md +++ b/docs/14-design-system.md @@ -41,7 +41,7 @@ Principles: ## UI kit - `src/ui/` holds the components (`import { … } from "../ui"`): - Icon (24 line icons), Wordmark, Button/ButtonLink, IconButton, + Icon (26 line icons), Wordmark, Button/ButtonLink, IconButton, Chip, Tag, Avatar/AvatarStack, TextField, Hint, Sticker, Pin, CreditLine, SceneCard, Developing, BoardHeader, SectionHead, FilterChips, Dock, Sheet, EmptyState, Menu, SkinPicker, TryOnBar, diff --git a/docs/17-showcase-sets.md b/docs/17-showcase-sets.md index 17374e64..2dfd65b9 100644 --- a/docs/17-showcase-sets.md +++ b/docs/17-showcase-sets.md @@ -34,9 +34,38 @@ Related: | 8 | Check | tests, phone and desktop screenshots | the PR | | 9 | Record provenance | per-set table and prompts | `public/landing/README.md` | -Steps 2 to 6 run on scratch scripts today. PIN-172 adds the set -manifest (`eval/showcase/sets.json`) and PIN-173 turns the driver into -`scripts/showcase-build.mjs`. This page is the contract they follow. +Steps 2 to 6 run with one script, `scripts/showcase-build.mjs` +(PIN-173; helpers in `scripts/showcase/`, tests next to them): + +| Step | Command | +| --- | --- | +| 2 | `photos --live` | +| 3 | `serve [--live --env-file ]` | +| 4 | `plan `, then `run [set...]` | +| 5 | `run` writes `contact-sheet.html`; `sheet ` rebuilds it | +| 6 | `web ` | + +Run it as `node scripts/showcase-build.mjs ` or +`npm run showcase -- `; `--help` prints the options. + +- **Paths and env.** Every path and the env file come from an option or + an environment variable (`--work-dir` / `SHOWCASE_WORK_DIR`, + `--photos-dir` / `SHOWCASE_PHOTOS_DIR`, `--base-url` / + `SHOWCASE_BASE_URL`, `--env-file` / `SHOWCASE_ENV_FILE`). The work + folder defaults to `.showcase-work/`, which git ignores. Nothing + about the operator's machine is written into the repo. +- **Paid calls need a flag.** `photos` only plans until you pass + `--live`. `serve` starts a mock app (`AI_MOCK=1`, free) unless you + pass `--live`. `run` refuses a live app unless you pass `--live`. +- **Specs hold no local paths.** A friend's photo is a relative name + inside the photos folder (`cat/aye.webp`), so a spec can be + committed. PIN-172's manifest (`eval/showcase/sets.json`) is read the + same way: `plan` and `run` take one set spec, or a manifest + `{ "sets": [ ... ] }` plus set ids. +- **Nothing secret in output.** Board slugs, session cookies and keys + are never printed, logged or written to `run.json`. + +This page stays the contract the script follows. ## 1. Write the brief @@ -99,6 +128,33 @@ returns 1280x1920 or 1920x1280 **WebP**, whatever name you save it under. Save it as `.webp`: the upload takes the picture's type from the file extension. +The `photos` command does this. Its items file has one entry per photo, +named relative to the photos folder: + +```json +[ + { "out": "cat/aye.webp", "size": "1536x1024", "prompt": "Casual smartphone photo of ..." } +] +``` + +```sh +node scripts/showcase-build.mjs photos items.json --env-file --live +``` + +- It makes one photo at a time and saves each under its real type + (WebP, whatever name you asked for). +- It skips a photo that already exists; `--force` makes it again. +- It counts every generated image in `photos-state.json` in the work + folder, against `--image-cap` (40 by default, PIN-172's cap), and + stops before a call that would pass the cap. Start the count from the + images made elsewhere with `--baseline N` on the first run. A call + whose picture cannot be fetched or is not an image still counts (it + was paid for), is reported as failed, and the next photo carries on. +- The state file also keeps each image's model, size, request id and + time, for the provenance table. +- Without `--live` it only prints the plan. The log shows status, + time and size, never the key, a prompt or an answer. + What cuts well, from the runs so far: - **Contrast.** Put a thing on a plain surface of another colour. A @@ -118,9 +174,16 @@ What cuts well, from the runs so far: ## 3. Run the app locally with live models Run the worktree's app on `localhost:3000` with the operator's env -file loaded at start-up. For example, a small launcher outside the -repo calls `process.loadEnvFile()`, sets the overrides below and -starts `next dev`. +file loaded at start-up: + +```sh +node scripts/showcase-build.mjs serve --live --env-file +``` + +`serve` loads the env file, forces the overrides below, and starts +`next dev` listening on localhost only. Without `--live` it starts a +mock app (`AI_MOCK=1`): free, and its scenes are collages. Use that +for a dry run of the whole chain before any live run. | Variable | Value | Why | | --- | --- | --- | @@ -133,8 +196,11 @@ starts `next dev`. | `AI_DAILY_CALL_LIMIT` | `2000` | room for a full run | Also unset the Stripe variables, `DEMO_BOARD_SLUG` and -`FIRESTORE_EMULATOR_HOST`. Live calls run only on the operator's -machine. CI stays on `AI_MOCK=1`. +`FIRESTORE_EMULATOR_HOST` (`serve` does). A live server also gets +longer model timeouts through `POLICY_OVERRIDES_JSON` (Spark 90 s, +SAM 60 s, scene 180 s; a longer value in the env file wins), and a +throwaway `SESSION_SECRET` when the env has none. Live calls run only on +the operator's machine. CI stays on `AI_MOCK=1`. Limits that matter on a local run: @@ -152,8 +218,14 @@ Limits that matter on a local run: ## 4. Drive one board per set -Drive the local app over its public boards API, one board per set. -These are the same calls `scripts/seed-demo-board.mjs` makes: +```sh +node scripts/showcase-build.mjs plan spec.json # no network: checks the spec and the photos +node scripts/showcase-build.mjs run spec.json cat --live # one set; --live only for a live app +``` + +`run` drives the local app over its public boards API, one board per +set, and sets run one after another. These are the same calls +`scripts/seed-demo-board.mjs` makes: 1. `POST /api/boards` with `{ title, themeId }` creates the board. Keep its slug in memory only. A board slug is the board's access @@ -179,16 +251,17 @@ Pick a scene's elements by a pattern on the label or description, not by an exact label. Spark names the same thing differently from run to run ("Smiling Woman", then "Selfie Woman"). -The spike's driver read a spec like this one, with photo paths -pointing into the work folder: +`run` reads a set spec like this one. Photo names are relative to the +photos folder, so the spec holds no local path: ```json { + "id": "cat", "title": "Showcase: cat (synthetic)", "friends": [ - { "name": "Aye", "photo": "/cat.webp" }, - { "name": "Ben", "photo": "/beanie.webp" }, - { "name": "Kim", "photo": "/skateboard.webp" } + { "name": "Aye", "photo": "cat/aye.webp" }, + { "name": "Ben", "photo": "cat/ben.webp" }, + { "name": "Kim", "photo": "cat/kim.webp" } ], "scenes": [ { @@ -199,20 +272,63 @@ pointing into the work folder: { "friend": "Ben", "match": "beanie|hat" }, { "friend": "Ben", "match": "heart|sunglass|glasses" }, { "friend": "Kim", "match": "skate|board" } - ] + ], + "names": ["cat", "beanie", "sunglasses", "skateboard"] } ] } ``` -It wrote the downloads and a run record next to them: status and time -per photo, labels, face flags, and each scene's status, kind and -prompt version. Keep all of it in the work folder. +The script checks the spec before it starts, so a mistake costs nothing: + +- 2 to 6 friends with made-up first names, and at most 3 scenes. +- A scene prompt is at most 500 characters and has 1 to 4 picks + (`Policy.pinsPerScene`), from at least 2 friends. +- The prompt must name every picked thing (each `match` must match + the prompt). +- **Every item the prompt names must be a picked pin.** List those + items in `names`: each must be in the prompt and match some pick. + Muse Image draws an unpinned item from the text alone (PIN-171). +- A scene runs only if every pick found a thing in the photos. A pick + prefers a label match to a description match, and one thing never + fills two picks. + +What `run` does and refuses: + +- It only talks to `localhost`, `127.0.0.1` or `[::1]`. A deployed app + needs `--allow-remote-base`. +- It reads `/api/health` first. A live app (`aiMock: false`) needs + `--live`, because the run then makes paid calls. +- It retries a failed photo once and logs each job's `errorCode`. A + refusal is not retried. It tries a scene up to `--scene-tries` times + (3) when it is refused, failed or came back as a collage. +- It counts every photo and scene job against `--max-jobs` (24), and + refuses to start if the sets need more than that at the least. +- It fails loudly (exit 1) on a scene that is refused or, on a live + app, a collage; a photo with no usable pin; a pick with no thing; + and a run over the cap. It still writes `run.json` and the contact + sheet, so the evidence is kept. After a failing set it stops + without starting the next. +- A set's folder `out//` that already exists is moved aside to + `out/-