deckhearth/.convoys/redesign-scanner-flow.md

161 lines
5.7 KiB
Markdown
Raw Normal View History

---
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-<pr>`.
**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.