---
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.
