feat(scanner): persist scan captures to Blob (Brief 3) #44

Merged
varutasu merged 1 commit from feat/redesign-scanner-flow-brief-3 into main 2026-05-27 14:57:26 -04:00
varutasu commented 2026-05-27 14:55:37 -04:00 (Migrated from github.com)

Summary

  • Adds POST /api/scan/upload-image (auth + upload rate limit) to store confirmed scan frames in Vercel Blob at scans/{userId}/{uuid}.jpg
  • CameraScanner uploads the capture on confirm and passes scanImageUrl through to the scanner queue
  • Migration adds user_cards.scan_image_url; POST /api/user-cards accepts and persists it when adding to owned cards

Completes the redesign-scanner-flow convoy (Briefs 1–3).

Test plan

  • Run npm run migrate up locally (adds scan_image_url column)
  • Scan a catalog card with destination = Owned; confirm row in user_cards has scan_image_url
  • Verify upload rate limit returns 429 after 10 uploads/hour
  • CI: vitest + preview smoke + visual diff (CameraScanner touched)

Made with Cursor

## Summary - Adds `POST /api/scan/upload-image` (auth + upload rate limit) to store confirmed scan frames in Vercel Blob at `scans/{userId}/{uuid}.jpg` - `CameraScanner` uploads the capture on confirm and passes `scanImageUrl` through to the scanner queue - Migration adds `user_cards.scan_image_url`; `POST /api/user-cards` accepts and persists it when adding to owned cards Completes the `redesign-scanner-flow` convoy (Briefs 1–3). ## Test plan - [ ] Run `npm run migrate up` locally (adds `scan_image_url` column) - [ ] Scan a catalog card with destination = Owned; confirm row in `user_cards` has `scan_image_url` - [ ] Verify upload rate limit returns 429 after 10 uploads/hour - [ ] CI: vitest + preview smoke + visual diff (CameraScanner touched) Made with [Cursor](https://cursor.com)
vercel[bot] commented 2026-05-27 14:55:44 -04:00 (Migrated from github.com)

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
tcg-vault Ready Ready Preview, Comment May 27, 2026 6:55pm

Request Review

[vc]: #NaWChg+3Z1m5q2tv1I3CBL6SbabVbs8/FO0qu1/VeK8=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ0Y2ctdmF1bHQiLCJwcm9qZWN0SWQiOiJwcmpfRjZXOEVvRkd3Y0g3aWVGcnRvRlNlOXdVVkFhNSIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9yYW5kYWxsLXN0aWxsd2VsbHMtcHJvamVjdHMvdGNnLXZhdWx0LzhjUGlOR05oV2Q4a1ZMZ0RHaUpHazM1c3dvamUiLCJwcmV2aWV3VXJsIjoidGNnLXZhdWx0LWdpdC1mZWF0LXJlZGVzaWduLTJhOWNjOC1yYW5kYWxsLXN0aWxsd2VsbHMtcHJvamVjdHMudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJ0Y2ctdmF1bHQtZ2l0LWZlYXQtcmVkZXNpZ24tMmE5Y2M4LXJhbmRhbGwtc3RpbGx3ZWxscy1wcm9qZWN0cy52ZXJjZWwuYXBwIn0sInJvb3REaXJlY3RvcnkiOm51bGx9XSwicmVxdWVzdFJldmlld1VybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS92ZXJjZWwtYWdlbnQvcmVxdWVzdC1yZXZpZXc/b3duZXI9dmFydXRhc3UmcmVwbz10Y2ctdmF1bHQmcHI9NDQifQ== The latest updates on your projects. Learn more about [Vercel for GitHub](https://vercel.link/github-learn-more). | Project | Deployment | Actions | Updated (UTC) | | :--- | :----- | :------ | :------ | | [tcg-vault](https://vercel.com/randall-stillwells-projects/tcg-vault) | ![Ready](https://vercel.com/static/status/ready.svg) [Ready](https://vercel.com/randall-stillwells-projects/tcg-vault/8cPiNGNhWd8kVLgDGiJGk35swoje) | [Preview](https://tcg-vault-git-feat-redesign-2a9cc8-randall-stillwells-projects.vercel.app), [Comment](https://vercel.live/open-feedback/tcg-vault-git-feat-redesign-2a9cc8-randall-stillwells-projects.vercel.app?via=pr-comment-feedback-link) | May 27, 2026 6:55pm | <a href="https://vercel.com/vercel-agent/request-review?owner=varutasu&repo=tcg-vault&pr=44" rel="noreferrer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://agents-vade-review.vercel.sh/request-review-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://agents-vade-review.vercel.sh/request-review-light.svg"><img src="https://agents-vade-review.vercel.sh/request-review-light.svg" alt="Request Review"></picture></a>
github-actions[bot] commented 2026-05-27 14:55:51 -04:00 (Migrated from github.com)

Pipeline Health

Build + CI gates

Gate Status
Vercel build (Preview) pass
CI: Lint pass
CI: Schema map fresh skipped
Preview smoke pass
Visual diff pass

Build runs on Vercel; this CI runs lint and schema-map drift only (no duplicate build).

Role reports

Role Status
Reviewer report pending
A11y audit pending
Design system audit pending

See individual comments above for details. This rollup updates automatically.

<!-- pipeline-rollup --> ## Pipeline Health ### Build + CI gates | Gate | Status | | --- | --- | | Vercel build (Preview) | ✅ pass | | CI: Lint | ✅ pass | | CI: Schema map fresh | ❌ skipped | | Preview smoke | ✅ pass | | Visual diff | ✅ pass | _Build runs on Vercel; this CI runs lint and schema-map drift only (no duplicate build)._ ### Role reports | Role | Status | | --- | --- | | Reviewer report | ⏳ pending | | A11y audit | ⏳ pending | | Design system audit | ⏳ pending | See individual comments above for details. This rollup updates automatically.
github-actions[bot] commented 2026-05-27 14:56:51 -04:00 (Migrated from github.com)

Visual Diff

Screenshots and diffs uploaded as artifacts: view run

If intentional changes: update snapshots locally with npx playwright test --project=visual --update-snapshots and commit.

## Visual Diff Screenshots and diffs uploaded as artifacts: [view run](https://github.com/varutasu/tcg-vault/actions/runs/26532081202) If intentional changes: update snapshots locally with `npx playwright test --project=visual --update-snapshots` and commit.
varutasu commented 2026-05-27 15:09:26 -04:00 (Migrated from github.com)

Reviewer Report

Check Status Notes
Scope match All 8 brief-listed files changed; migration + SCHEMA_MAP updated; no unexpected files touched
Conventions ⚠️ Auth/DB/rate-limit/theme-token patterns correct; condition/is_foil silently dropped for collection and deck destinations (see Finding 1)
Security upload-image.js is auth-gated, rate-limited (checkUploadRateLimit), validates MIME type via regex, enforces 5 MB buffer cap
Regression risk medium emitScannedCard made async in a scan pipeline; handleCardScanned reads scannedCards outside setState callback (stale-closure risk on rapid scans); heavy scanner.js rewrite
Test coverage ⚠️ Vitest 21/21 green (AC #5 ); zero new tests for ScannedCardItem, ScannerDestinationPicker, or upload-image.js
Documentation docs/SCHEMA_MAP.md updated with scan_image_url; migration file self-documents

Findings

  • 🟡 AC #2 partially satisfied — condition/is_foil silently dropped for collection and deck destinations. Both collections/[identifier]/cards.js and decks/[id]/cards.js now accept condition and is_foil in the request body, but neither collection_cards nor deck_cards has those columns. For collections, the values are logged to activity only (not in the SQL UPDATE/INSERT). For decks, the values are echoed in the response metadata object only (not stored). Only user_cards actually persists them. If condition/foil-per-collection-slot is a product goal, a follow-up migration is needed; if it's intentional (card-level metadata lives on user_cards), document it and strip the unused params from the two handlers to avoid misleading callers. Suggested convoy: add-condition-foil-to-collection-deck-schema or a doc note in AGENTS.md.

  • 🟡 user-cards.js missing the parseInt/NaN guard added in the decks handler. decks/[id]/cards.js now validates quantity with parseInt(quantity, 10) + isNaN check. user-cards.js still adds raw quantity directly to the existing row's count. Suggested follow-up: add parseInt/NaN guard to user-cards.js, matching the decks handler.

  • 🟢 scan_image_url stored without origin validation. Low risk today; worth an origin prefix check in a future hardening pass.

  • 🟢 No unit tests for new surfaces. Suggested convoy: test-scanner-redesign-surfaces.

  • 🟢 Stale-closure risk in handleCardScanned on rapid-fire scans. Fixable with a useRef queue or moving dedup inside the setScannedCards updater.

Approval recommendation

  • comment-only (post-merge audit — code already in main; findings should be captured as follow-up convoy tasks rather than reverts)
## Reviewer Report | Check | Status | Notes | | --- | --- | --- | | Scope match | ✅ | All 8 brief-listed files changed; migration + SCHEMA_MAP updated; no unexpected files touched | | Conventions | ⚠️ | Auth/DB/rate-limit/theme-token patterns correct; `condition`/`is_foil` silently dropped for collection and deck destinations (see Finding 1) | | Security | ✅ | `upload-image.js` is auth-gated, rate-limited (`checkUploadRateLimit`), validates MIME type via regex, enforces 5 MB buffer cap | | Regression risk | medium | `emitScannedCard` made async in a scan pipeline; `handleCardScanned` reads `scannedCards` outside `setState` callback (stale-closure risk on rapid scans); heavy scanner.js rewrite | | Test coverage | ⚠️ | Vitest 21/21 green (AC #5 ✅); zero new tests for `ScannedCardItem`, `ScannerDestinationPicker`, or `upload-image.js` | | Documentation | ✅ | `docs/SCHEMA_MAP.md` updated with `scan_image_url`; migration file self-documents | ### Findings - 🟡 **AC #2 partially satisfied — `condition`/`is_foil` silently dropped for collection and deck destinations.** Both `collections/[identifier]/cards.js` and `decks/[id]/cards.js` now accept `condition` and `is_foil` in the request body, but neither `collection_cards` nor `deck_cards` has those columns. For collections, the values are logged to activity only (not in the SQL `UPDATE`/`INSERT`). For decks, the values are echoed in the response `metadata` object only (not stored). Only `user_cards` actually persists them. If condition/foil-per-collection-slot is a product goal, a follow-up migration is needed; if it's intentional (card-level metadata lives on `user_cards`), document it and strip the unused params from the two handlers to avoid misleading callers. **Suggested convoy: `add-condition-foil-to-collection-deck-schema` or a doc note in AGENTS.md.** - 🟡 **`user-cards.js` missing the `parseInt`/NaN guard added in the decks handler.** `decks/[id]/cards.js` now validates `quantity` with `parseInt(quantity, 10)` + `isNaN` check. `user-cards.js` still adds raw `quantity` directly to the existing row's count. **Suggested follow-up: add parseInt/NaN guard to `user-cards.js`, matching the decks handler.** - 🟢 **`scan_image_url` stored without origin validation.** Low risk today; worth an origin prefix check in a future hardening pass. - 🟢 **No unit tests for new surfaces.** Suggested convoy: `test-scanner-redesign-surfaces`. - 🟢 **Stale-closure risk in `handleCardScanned` on rapid-fire scans.** Fixable with a `useRef` queue or moving dedup inside the `setScannedCards` updater. ### Approval recommendation - **comment-only** (post-merge audit — code already in `main`; findings should be captured as follow-up convoy tasks rather than reverts)
varutasu commented 2026-05-27 15:09:32 -04:00 (Migrated from github.com)

Design System Audit

Check Status Count
Token violations 7
Duplicate primitives 1
Missing variants 0
Inline styles 4

Token violations

All violations are in components/CameraScanner.js (pre-existing overlay chrome, not introduced by Briefs 1–3):

  • 844–846 — Card detection overlay borderColor uses hardcoded #10B981 / #3B82F6. Use semantic CSS variables or Tailwind border classes.
  • 848–851boxShadow uses 8-digit hex literals #10B98150 / #3B82F650.
  • 859–862 — Status-label backgroundColor repeats the same hex literals.
  • 907 — Stop-camera button backgroundColor: rgba(239, 68, 68, 0.9) → use bg-red-500/90.
  • 908border: 3px solid rgba(255, 255, 255, 0.3)border-white/30.
  • 946 — Start-camera button border: 4px solid rgba(255, 255, 255, 0.2)border-white/20.
  • 952 — Decorative radial gradient with literal rgba (lowest priority).

Duplicate primitives

  • 1027–1091 — Disambiguation modal is inline; repo has CollectionSelectionModal.js / ShareModal.js as modal shells. Flag for a shared modal wrapper convoy.

Other findings

  • ScannedCardItem.js, ScannerDestinationPicker.js, pages/scanner.js — Clean. All colors via var(--*) tokens; spacing via Tailwind utilities.
  • Modal backdrops use bg-black bg-opacity-* (pre-existing pattern across scanner modals).
## Design System Audit | Check | Status | Count | | --- | --- | --- | | Token violations | ❌ | 7 | | Duplicate primitives | ❌ | 1 | | Missing variants | ✅ | 0 | | Inline styles | ❌ | 4 | ### Token violations All violations are in `components/CameraScanner.js` (pre-existing overlay chrome, not introduced by Briefs 1–3): - **844–846** — Card detection overlay `borderColor` uses hardcoded `#10B981` / `#3B82F6`. Use semantic CSS variables or Tailwind border classes. - **848–851** — `boxShadow` uses 8-digit hex literals `#10B98150` / `#3B82F650`. - **859–862** — Status-label `backgroundColor` repeats the same hex literals. - **907** — Stop-camera button `backgroundColor: rgba(239, 68, 68, 0.9)` → use `bg-red-500/90`. - **908** — `border: 3px solid rgba(255, 255, 255, 0.3)` → `border-white/30`. - **946** — Start-camera button `border: 4px solid rgba(255, 255, 255, 0.2)` → `border-white/20`. - **952** — Decorative radial gradient with literal rgba (lowest priority). ### Duplicate primitives - **1027–1091** — Disambiguation modal is inline; repo has `CollectionSelectionModal.js` / `ShareModal.js` as modal shells. Flag for a shared modal wrapper convoy. ### Other findings - **`ScannedCardItem.js`, `ScannerDestinationPicker.js`, `pages/scanner.js`** — Clean. All colors via `var(--*)` tokens; spacing via Tailwind utilities. - Modal backdrops use `bg-black bg-opacity-*` (pre-existing pattern across scanner modals).
varutasu commented 2026-05-27 15:09:33 -04:00 (Migrated from github.com)

A11y Audit

Check Status Count
Labels 7
Keyboard nav 0
Focus management 2
Color contrast ⚠️ 1
Semantic HTML ⚠️ 2
ARIA correctness 3
UX constraint match 1

Critical (must fix)

  • ScannedCardItem.js:115aria-label on bare <div> is dropped by AT; ownership badge inaccessible — add role="status".
  • ScannedCardItem.js:226 — Remove button is emoji-only — add aria-label="Remove card".
  • ScannedCardItem.js:236 — Collection <select> unlabeled — add aria-label.
  • ScannedCardItem.js:259 — Deck <select> unlabeled — add aria-label.
  • CameraScanner.js:941 — Start camera button is icon-only — add aria-label="Start camera".
  • CameraScanner.js:903 — Stop camera button is icon-only — add aria-label="Stop camera".
  • CameraScanner.js:1027 — Disambiguation dialog missing focus trap / restore on close.
  • pages/scanner.js:716 — Create Collection modal missing role="dialog", aria-modal, focus trap.
  • Bulk toolbar selects and Clear Selection button need aria-labels (scanner.js:651, 672, 699).
  • Heading hierarchy skip in CameraScanner detection panel (h4 after page h2).
  • <video> feed needs aria-label="Card scanner camera feed".
  • Scanned cards queue should use list semantics (<ul>/<li>).
  • Queue count should use aria-live="polite".

Notes

  • ScannerDestinationPicker.js is clean — native buttons with aria-pressed, fieldset/legend, labeled selects. UX constraint "destination picker keyboard-operable" .
  • scanNotice / autoRouteError live regions are correctly implemented.
## A11y Audit | Check | Status | Count | | --- | --- | --- | | Labels | ❌ | 7 | | Keyboard nav | ✅ | 0 | | Focus management | ❌ | 2 | | Color contrast | ⚠️ | 1 | | Semantic HTML | ⚠️ | 2 | | ARIA correctness | ❌ | 3 | | UX constraint match | ❌ | 1 | ### Critical (must fix) - `ScannedCardItem.js:115` — `aria-label` on bare `<div>` is dropped by AT; ownership badge inaccessible — add `role="status"`. - `ScannedCardItem.js:226` — Remove button is emoji-only — add `aria-label="Remove card"`. - `ScannedCardItem.js:236` — Collection `<select>` unlabeled — add `aria-label`. - `ScannedCardItem.js:259` — Deck `<select>` unlabeled — add `aria-label`. - `CameraScanner.js:941` — Start camera button is icon-only — add `aria-label="Start camera"`. - `CameraScanner.js:903` — Stop camera button is icon-only — add `aria-label="Stop camera"`. - `CameraScanner.js:1027` — Disambiguation dialog missing focus trap / restore on close. - `pages/scanner.js:716` — Create Collection modal missing `role="dialog"`, `aria-modal`, focus trap. ### Warnings (recommended) - Bulk toolbar selects and Clear Selection button need `aria-label`s (`scanner.js:651`, `672`, `699`). - Heading hierarchy skip in CameraScanner detection panel (`h4` after page `h2`). - `<video>` feed needs `aria-label="Card scanner camera feed"`. - Scanned cards queue should use list semantics (`<ul>`/`<li>`). - Queue count should use `aria-live="polite"`. ### Notes - **`ScannerDestinationPicker.js` is clean** — native buttons with `aria-pressed`, fieldset/legend, labeled selects. UX constraint "destination picker keyboard-operable" ✅. - `scanNotice` / `autoRouteError` live regions are correctly implemented.
Sign in to join this conversation.
No description provided.