# BoxSeek QR tutorial capture recipe

This is the playbook for the public how-tos at **https://boxseekqr.com/tutorial/**.
The site is static HTML plus phone screenshots. Screenshots are **not** taken by
hand: a headless Chrome session drives the **local web build** (`dist/`) through
Chrome DevTools Protocol (CDP) Fetch, the same pattern as `scripts/browser-smoke.mjs`.

Do **not** start `expo start`, `npm run web`, or `npm run preview` for capture or
to “check” the app. Capture serves `dist/` itself. After HTML/shot changes, publish
`website/tutorial/` (this folder only—not a full `/app` deploy).

## Why this works

1. **One continuous inventory.** `recipes/tutorials.json` is a single ordered
   script. The first action is `setup:demo`. Later tutorials reuse the box and
   item created earlier (`Camping gear` → `Wool sweater`). Do not shuffle steps
   without checking later `wait:` / `click:` labels.
2. **The harness never hits paid APIs.** CDP Fetch intercepts `localhost` and
   `/api/*`. App-control is mocked as clear-to-run. Credits (`/api/credits`)
   return a guest-shaped UUID account so Account details is readable offline.
   Other `/api/*` calls get 503. Open-Meteo geocoding (Find City) is allowed
   because it is a public hostname, not `/api/`.
3. **Clicks target live accessibility labels.** Recipe strings must match the
   current UI (`New box`, `Take Photo`, `Who has it?`). When a step times out,
   the script dumps `document.body.innerText` so you can fix the label.
4. **Shots are committed with the HTML** so `website/` is a restore-ready copy
   of `/tutorial/` on the live host.

## Prerequisites

1. `npm run build:web` (writes `dist/`). Rebuild if the app UI changed.
2. Google Chrome at `/opt/google/chrome/chrome` (or `CHROME_PATH`).
3. Node 24+ (same as the rest of the repo).
4. `assets/photos/tutorial-grey-sweater.jpg` — still used as the Take photo
   camera frame for the new Wool sweater (not the cream demo sweater).
5. `assets/photos/winter-box-clean.png` — same tote photo as `ShelfScene`; the
   harness draws the same printed label + live QR as the box screen for `scanqr:*`.

## Commands

From the repo root:

```sh
npm run build:web          # only if dist/ is missing or the app changed
npm run capture:tutorial   # node scripts/capture-tutorial.mjs
```

Then publish (see below).

## Folder layout

```
website/tutorial/
  TUTORIAL-RECIPE.md          ← this document
  index.html                  ← hub
  tutorial.css                ← phone-frame layout (420/844 screenshots in phone-device.svg bezel)
  phone-device.svg            ← titanium-style device overlay (Dynamic Island, buttons, lip)
  make-a-box.html
  add-an-item.html
  print-labels.html
  scan-label.html
  search.html
  lending.html
  moving.html
  alerts.html
  pinned.html
  more.html
  account.html
  database.html               ← placeholder; no shots yet
  weather-seasons.html
  demo-intro.html
  recipes/tutorials.json      ← ordered capture plan + viewport
  shots/                      ← PNGs named {id}.png
```

Related files outside this folder:

| Path | Role |
|------|------|
| `scripts/capture-tutorial.mjs` | CDP driver |
| `assets/photos/tutorial-grey-sweater.jpg` | Fake-camera still (Take photo) |
| `assets/photos/winter-box-clean.png` | Scan camera still (`ShelfScene` label + live QR) |
| `website/.htaccess` | `/tutorial/` index.html → `/tutorial/` 301 |
| `package.json` `capture:tutorial` | npm alias |

## Viewport and images

`recipes/tutorials.json` sets `"viewport": { "width": 420, "height": 844 }`.
Chrome is emulated as mobile with `deviceScaleFactor: 2`, so PNGs are
**840 × 1688**. HTML `<img>` tags use `width="420" height="844"` so the phone
frame matches CSS `aspect-ratio: 420/844` in `tutorial.css`.

If you change the viewport, update those three places together (JSON, CSS,
every `<img>`).

## Recipe file

Each step:

| Field | Meaning |
|-------|---------|
| `id` | Shot filename stem (`10-item-with-photo` → `shots/10-item-with-photo.png`) |
| `tutorial` | Which HTML page consumes the shot |
| `title` / `body` | Copy used while authoring; HTML may paraphrase but must not invent UI |
| `actions` | Commands run **before** the screenshot |

### Actions

- `setup:demo` — mark intro + sample tip seen, keep demo inventory, reload.
  Must be the first action after launch.
- `wait:<text>` — `document.body.innerText` contains the string.
- `click:<label>` — last visible `[role=button|checkbox|radio]`, `button`, or
  `a` whose text or `aria-label` matches (case-insensitive). Prefers an
  unchecked checkbox, then a radio/checkbox (mode pills beat footer nav with
  the same word, e.g. Search → **Boxes**).
- `tap:<label>` — CDP mouse click on a named control (use when a zero-size
  Pressable or checkbox needs a real mouse event).
- `field:<label>=<value>` — fill the input/textarea with that `aria-label`.
- `pickdate:<label>=YYYY-MM-DD` — open a DateField calendar and pick the day
  (advances months if needed).
- `scrollinto:<text>` — scroll matching label/text into view (nested ScrollViews too).
- `scrollpage:bottom` / `scrollpage:top` — scroll nested ScrollViews to an end.
- `sleep:<ms>` — wait for layout.
- `fake:camera` — replace `getUserMedia` with a canvas stream of
  `assets/photos/tutorial-grey-sweater.jpg`.
- `fake:install` — synthesize `beforeinstallprompt` so More shows
  **Install BoxSeek QR** / **Install app** (Chrome/Edge path).
- `clear:install` — fire `appinstalled` so the Install card hides (iOS /
  already-installed look).
- `scanqr:box:<name>` / `scanqr:item:<name>` — look up the record in IndexedDB,
  encode `boxseekqr-next://{kind}/{labelId}`, compose the same shelf + sticker
  layout as `src/ui/shelf.tsx`, and feed that frame to Scan.

### Click gotchas

- Action titles are title-case in the DOM (`Take Out`, `Take Photo`). Prefer
  the `aria-label` the app actually sets.
- Footer **Boxes** vs Search-mode **Boxes**: the harness prefers `role=radio`
  / `checkbox`.
- After **Replay introduction**, **Skip** returns to **Home**, not More. Open
  More again before Introduction and sample.
- **Remove sample inventory** is captured on the confirm sheet; do not confirm
  in the recipe (later shots would lose demo data). Cancel by leaving the sheet
  or by not clicking Confirm.

## Harness behaviour (`scripts/capture-tutorial.mjs`)

- Fresh Chrome profile under `/tmp/findit-tutorial-*`.
- Fetch.enable for all URLs.
- `localhost` paths: `/app/…` is stripped so files come from `dist/`.
- `/api/app-control` → `{ revision: "tutorial-capture", … }`.
- `/api/credits*` → guest snapshot with UUID `accountId` (must match
  `ACCOUNT_ID` in `src/inventory.ts` or Account details shows
  “Invalid credit identity.”).
- Other `/api/*` → 503.
- Non-localhost (Open-Meteo, fonts) → `Fetch.continueRequest`.
- Service workers unregistered so a stale shell cannot cache.
- Overall timeout 600s. Per-wait ~25s, then dump page text.

## Continuous story (keep these names)

Demo already has Winter clothes, Holiday decorations, Travel, Passport, Keys,
Red scarf, Wool sweater (cream, lent to Alex), Pain reliever, AA batteries.

The recipe **adds**:

- Box **Camping gear** at **Garage shelf**
- Item **Wool sweater** (grey photo, qty 2, description/color/size/barcode/notes,
  cold+heat, Winter, pinned)

Then it **lends** one unit to **Jordan** (return 2027-03-20), **returns** it,
**moves** it to **Winter clothes**, **takes out** / **puts back**, **pins
Red scarf**, sets weather city **Toronto**, and opens intro replay / remove-sample
confirm **without** deleting the samples.

If you rename any of those strings, update every later `wait:`/`click:`/`field:`
and the HTML copy.

## HTML pages

Shared sales header (`../styles.css`, `../site.js`, theme toggle). Main column
is phone-frame + copy; aside is on-this-page links. Footer nav points to the
previous and next how-to. **Never invent UI the capture did not show.**

When you add a tutorial:

1. Append steps to `recipes/tutorials.json` (`tutorial` slug = page).
2. `npm run capture:tutorial` (full run; the story is sequential).
3. Add `website/tutorial/<slug>.html` with `shots/{id}.png`.
4. Link it from `index.html` and the previous page’s “next” nav.
5. List it under **Current tutorials** below.
6. Publish.

## Item photo (not the cream demo sweater)

Headless Chrome has no camera. The first version of Take photo drew a grey
silhouette labeled “Simple item photo”. That still leaked into add-item, search,
lend, and move shots.

**Now:** `fake:camera` paints `assets/photos/tutorial-grey-sweater.jpg`
(folded grey cable-knit, matches Color = Grey on the added item). The demo
Wool sweater in Travel/Winter clothes stays the cream `assets/photos/sweater.jpg`.
Search can show **both**.

To change the added-item photo, replace that JPEG and recapture. Do not draw
shapes or overlay title text on the still.

## Publish

Tutorials are static under the marketing docroot, **not** the `/app` container:

```sh
rsync -az --rsync-path='sudo -n -u boxseekqr rsync' \
  website/tutorial/ \
  ubuntu@themindfactory.com:/home/boxseekqr/public_html/tutorial/
```

Canonical hub: `https://boxseekqr.com/tutorial/`

`.htaccess` already 301s `/tutorial/index.html` → `/tutorial/`.
`tutorial/.htaccess` shortens PNG cache on origin; Cloudflare may still cache
for a day. After changing shots, bump `?v=` on the page’s `<img src>` URLs (see
`scan-label.html`) so browsers and the CDN fetch the new PNG.

Verify after rsync: hub links, a new HTML page, and one new PNG (`curl -sI`).

## Troubleshooting

| Symptom | Fix |
|---------|-----|
| `Missing dist/` | `npm run build:web` |
| Timed out: *label* | Dump is the current screen; fix `wait`/`click` to live copy |
| Click hits footer instead of a pill | Prefer exact `aria-label`; harness already prefers radio/checkbox |
| Shot cropped | `scrollinto:` / `scrollpage:bottom` before the shot |
| Grey sweater blob | Confirm `tutorial-grey-sweater.jpg` exists; recapture after `fake:camera` |
| Invalid credit identity | Credits mock `accountId` must be a UUID |
| Account details empty / error | `/api/credits` mock; do not 503 that path |
| Find City never lists a place | Open-Meteo must be reachable (not mocked) |
| Skip after intro lands on Home | `click:More` again (already in demo-intro recipe) |
| Capture overall timeout | 600s cap; split only if you also split the story |

## Current tutorials

| # | Page | Shots | What it shows |
|---|------|-------|----------------|
| — | `index.html` | `01-home-demo` | Hub + demo home |
| 01 | `make-a-box.html` | `02`–`05` | New box Camping gear / Garage shelf |
| 02 | `add-an-item.html` | `06`–`14` | Take photo (not Quick Add), details, save Wool sweater |
| 03 | `print-labels.html` | `15`–`20b` | Box/item labels, preview, setup; Advanced noted; Download/Print |
| 04 | `scan-label.html` | `21`–`24` | Affix, scan box, scan item |
| 05 | `search.html` | `25`–`30` | Items / Boxes / By location; why descriptive fields; Include removed |
| 06 | `lending.html` | `31`–`36` | Lend to Jordan, Lent out, Alerts, Returned |
| 07 | `moving.html` | `37`–`43` | Move, Take out, Taken out, Put back |
| 08 | `alerts.html` | `44`–`49` | Bell, expiring, open row, loans, unchecked box, forecast note |
| 09 | `pinned.html` | `50`–`54` | Home strip, Pin/Unpin, pin Red scarf, why pin |
| 10 | `more.html` | `55`–`57` | More hub, On this device, Database peek |
| 11 | `account.html` | `58`–`59` | Account fields; Activate / Move / refresh |
| 12 | `database.html` | *(none)* | Placeholder copy only — fill later |
| 13 | `weather-seasons.html` | `60`–`63` | City, Find City, save, reminder months |
| 14 | `demo-intro.html` | `64`–`67` | Replay intro, Skip, Remove sample confirm (not confirmed) |
| 15 | `home-screen.html` | `68`–`71` | Install app on Chrome/Edge; Safari Share on iOS; after install |

Keep demo names (`Winter clothes`, `Passport`, `Camping gear`, `Wool sweater`,
`Jordan`, `Red scarf`) identical across runs so screenshots stay comparable.
