--- name: redesign-scanner-flow classification: feature success_metric: | Scanner page uses a stack-destination model (one target at top, every scan pours into it); per-card condition + foil + quantity captured and propagate to destinations; "you already own N" ownership badge via /api/cards/[id]/ownership; captured frame persisted to Vercel Blob and attached to user_cards. skip: [] status: closed created: 2026-05-27 depends_on: - server-side-scan-pipeline --- # Convoy: redesign-scanner-flow Redesign the scanner session around a single destination stack, rich card metadata, ownership feedback, and persisted scan images. ## Why The current scanner treats each card as an isolated add with no session context: users re-select destination every scan, cannot set condition/foil/ quantity in bulk, get no feedback when they already own a card, and lose the captured frame after confirm. The audit flagged these as core UX gaps that block efficient bulk scanning at the table. ## Scope ### In scope - **Brief 1 — stack-destination UX + game pre-select** - `pages/scanner.js` — session model: one chosen target at top; every confirmed scan routes there. - `components/ScannerDestinationPicker.js` (new) — collection / deck / default collection picker with game filter. - **Brief 2 — condition / foil / quantity + ownership badge** - `pages/scanner.js`, `components/ScannedCardItem.js` (extract from scanner page). - `pages/api/user-cards.js`, `pages/api/decks/[id]/cards.js`, `pages/api/collections/[identifier]/cards.js` — accept `condition`, `is_foil`, `quantity` body params on POST. - Ownership badge via existing `pages/api/cards/[id]/ownership.js`. - **Brief 3 — captured image persistence** - `pages/api/scan/upload-image.js` (new) — auth + Blob upload. - `components/CameraScanner.js` — upload frame on confirm. - `pages/api/user-cards.js` — accept `scan_image_url`. ### Out of scope - **OCR / identify pipeline changes** — convoys #2 and #3. - **Vocabulary rename** ("My Collection" / "Lists") — convoy `rename-collections-vocabulary` (#5). - **Schema cleanup** — global `cards.quantity` removal is `schema-cleanup-from-scanner-audit`, not here. ## Roles invoked 1. `role-ia-architect` — destination stack model, nav labels. 2. `role-ux-reviewer` — bulk-scan flow, ownership badge placement. 3. `role-architect` — API param contract, 3 briefs. 4. `role-implementer` — 3 briefs (Briefs 2+3 parallel after Brief 1). 5. `role-reviewer` + `role-design-system-auditor` + `role-a11y-auditor`. ## Todos - [x] IA: stack-destination information architecture - [x] UX: condition/foil/quantity controls + ownership badge - [x] Architect: brief decomposition + API body-param contract - [x] Brief 1 — destination picker + session state (#42) - [x] Brief 2 — metadata + ownership (#43) - [x] Brief 3 — Blob persistence (#44) - [x] Post-PR audit — `audit-redesign-scanner-flow-44` (see `.convoys/redesign-scanner-flow/audit-redesign-scanner-flow-44.md`) ## Operator action required **None.** Assumes `BLOB_READ_WRITE_TOKEN` is already provisioned (avatar upload path uses Blob today). ## Multitask dispatch ### Slice dependencies ```yaml slice_dependencies: - brief: 1 depends_on: [] files: - pages/scanner.js - components/ScannerDestinationPicker.js - brief: 2 depends_on: [1] files: - pages/scanner.js - components/ScannedCardItem.js - pages/api/user-cards.js - pages/api/decks/[id]/cards.js - pages/api/collections/[identifier]/cards.js - brief: 3 depends_on: [1] files: - pages/api/scan/upload-image.js - components/CameraScanner.js - pages/api/user-cards.js ``` **After Brief 1 merges:** `/multitask role-implementer briefs 2, 3` (disjoint file sets except shared `pages/scanner.js` / `user-cards.js` — architect must resolve: likely Brief 2 owns `scanner.js` queue UI, Brief 3 owns CameraScanner + upload route only; adjust `files:` if conflict). Post-PR audit: ``` /multitask role-reviewer + role-design-system-auditor + role-a11y-auditor ``` Group id: `audit-redesign-scanner-flow-`. **Cross-convoy:** can run **parallel with #3** after #2 Brief 1+2 merge (disjoint primary surfaces). ## CI impact | Workflow / job | Behavior | | --- | --- | | `preview-smoke.yml` | Fires. | | `visual-diff.yml` | **Fires** — `pages/scanner.js`, `components/**` match paths; `!pages/api/**` still allows page changes through. | | New grep gates | None. | ## Decisions to ratify (architect) 1. **Default destination** — last-used vs explicit pick-required on session start. 2. **Condition enum** — align with existing `user_cards.condition` VARCHAR values. 3. **Blob path convention** — e.g. `scans/{userId}/{uuid}.jpg`. 4. **Brief 2 vs 3 file overlap** — split `pages/scanner.js` ownership to avoid multitask conflict. ## Known constraints - **Theme tokens** — no hardcoded hex; use `var(--*)` per ui-and-theming rule. - **Rate limits** — upload-image may need `checkUploadRateLimit` if architect classifies scan images as upload class (Decision pending). - **Mobile scanner** — destination picker must work on narrow viewports. ## Acceptance criteria 1. User selects one destination; all scans in session target it until changed. 2. Condition, foil, quantity propagate on add-to-collection/deck/user-cards. 3. Ownership badge shows when user already holds the card. 4. Confirmed scan image URL stored on `user_cards` row. 5. Lint + vitest baseline preserved. 6. A11y: destination picker keyboard-operable; badge has accessible text. ## Out of scope follow-ups - **`god-component-split`** — further split `pages/scanner.js` if still > 500 lines. - **`harden-multipart-parser`** — if upload-image uses multipart.