Commit graph

81 commits

Author SHA1 Message Date
Randall Stillwell
04504903bf perf(scanner): three high-impact speed optimizations
Some checks failed
CI / Lint (pull_request) Has been cancelled
CI / Schema map up to date (pull_request) Has been cancelled
CI / Forbidden patterns (9 checks) (pull_request) Has been cancelled
CI / Migrations apply (node-pg-migrate) (pull_request) Has been cancelled
CI / Unit tests (vitest) (pull_request) Has been cancelled
Convoy metrics gate / Require role-event telemetry on convoy PRs (pull_request) Has been cancelled
PR Health rollup / Aggregate gate status (pull_request) Has been cancelled
Preview smoke / Should run? (pull_request) Has been cancelled
Preview smoke / Playwright smoke (pull_request) Has been cancelled
1. Non-blocking scan capture upload (emitScannedCard)
   - Card result shows immediately; upload fires in background
   - Removes ~500ms blocking S3/MinIO roundtrip from the result path

2. Parallel OCR workers for name + number strips (ocr-worker)
   - Two independent Tesseract workers run simultaneously
   - Cuts Layer 1 OCR time ~50% on mobile (name + number in parallel)
   - terminateOcrWorker cleans up both workers on teardown

3. Faster pre-verification timing (scanner-card-detection)
   - VERIFICATION_INTERVAL_MS: 1000 → 500ms
   - MIN_FIRST_SEEN_MS_FOR_VERIFY: 800 → 500ms
   - Net ~300ms faster from first detection to verification start

All 58 scanner tests pass (2.20s).
2026-09-01 17:17:20 -05:00
Randall Stillwell
106bd9d592 feat(scanner): add debug instrumentation for vision pipeline timing
Some checks failed
CI / Lint (pull_request) Has been cancelled
CI / Schema map up to date (pull_request) Has been cancelled
CI / Forbidden patterns (9 checks) (pull_request) Has been cancelled
Visual diff / Should run? (pull_request) Has been cancelled
CI / Migrations apply (node-pg-migrate) (pull_request) Has been cancelled
Visual diff / Screenshot diff (pull_request) Has been cancelled
CI / Unit tests (vitest) (pull_request) Has been cancelled
Convoy metrics gate / Require role-event telemetry on convoy PRs (pull_request) Has been cancelled
PR Health rollup / Aggregate gate status (pull_request) Has been cancelled
Preview smoke / Should run? (pull_request) Has been cancelled
Preview smoke / Playwright smoke (pull_request) Has been cancelled
- Add isDebugMode() + debugLog() helpers to scanner-card-identify.js and use-scanner-identification.js
- Instrument Layer 0 (pgvector), Layer 1 (Tesseract OCR + pg_trgm), Layer 2 (Vision API) with per-layer timing
- Log shutter press, verification outcomes, rate-limit cooldowns, and pipeline totals
- Activate via localStorage.setItem('SCANNER_DEBUG', 'true') or window.__SCANNER_DEBUG = true
- Zero runtime overhead when debug mode is off (isDebugMode() check inlined)
- Add docs/SCANNER_DEBUG_MODE.md with full usage guide and log pattern examples
2026-09-01 17:02:01 -05:00
Randall Stillwell
a81c6dc21b fix(tests): wire DEFAULT_LAYOUT export, fix custom-frames mocks, and add validateLayout tests
- Export DEFAULT_LAYOUT from lib/frame-palette.js so the custom-frames
  route can import it at runtime (not just the hardcoded copy in tests).
- Fix vitest mock isolation in test/api/custom-frames.test.js: beforeEach now
  uses mockReturnValue instead of mockResolvedValue to avoid resolving the
  default mock in each test; test cases provide specific mock chains with
  mockResolvedValueOnce. Fixes 4 tests that were bleeding state between
  cases due to leftover queued mock values.
- Fix validateLayout test coordinates: art w+h=0.924 and 0.398 are both
  within the 0-1 fraction range so x+w=0.962<1 and y+h=0.982<1 pass.
- Add dedicated validateLayout unit tests (accepts, rejects missing zone,
  rejects out-of-bounds).
- Fix update test mock chain: PUT calls SELECT (found) then SELECT (clash)
  then UPDATE (RETURNING) — provide all three in order.
- Fix DELETE test: owns via SELECT then executes DELETE (2 calls).
2026-09-01 09:40:04 -05:00
Randall Stillwell
027ddcf83e feat(designer): image-based frame zones, ZoneEditor, custom frame/game APIs
Adds DEFAULT_LAYOUT + IMAGE_FRAME_ROWS constants (fractional art/text window
anchors), a ZoneEditor component for bounding-box layout editing in the
designer, and expands the custom-frames/games CRUD API surface to support
frame image storage and retrieval. Custom card designer pages wire these
together with the existing PNG export pipeline.

See .convoys/card-designer-image-frames.md for scope tracking.
2026-09-01 09:23:16 -05:00
Randall Stillwell
fe1695f7ad feat(designer): custom frame editor and cost symbols
- custom_frames: per-user frames with full 9-slot palette (JSONB),
  unique names; designer frame picker lists them alongside starters,
  click to use, edit/delete via inline editor with live preview
- custom_symbols: upload cost icons (PNG/WebP/SVG, 2MB) keyed by short
  code; re-uploading a code replaces the old icon; ManaPips renders
  icon pips for {CODE} tokens with graceful text fallback
- custom_cards.custom_frame_id links designs to custom frames; API GETs
  join and nest the palette; deleting a frame falls back to starter
- migration 1787700511000
2026-08-24 21:13:12 -05:00
Randall Stillwell
c0051dd6d5 feat(designer): game targeting and custom game spaces
- game system selector: standalone, existing system (MTG/Pokemon/Lorcana/
  SWU/FaB/One Piece/Sorcery/Grand Archive — codes match catalog imports),
  or a user's custom game
- custom_games table + CRUD API (private per user, unique names)
- /games hub with create form; /games/[id] space with rename, delete,
  card gallery, and ?game= deep-link into the designer
- catalog twin resolves game: system code, custom game name, or 'Custom'
- my-designs shows each design's game association
- migration 1787693311000
2026-08-24 21:01:04 -05:00
Randall Stillwell
147041d292 feat(designer): rarity badges, flavor quotes with dividers, full-art mode
- rarity now renders as shape+color symbol anchored inside the type bar
  (circle/diamond/pentagon/star per rarity) — fixes straddling gem alignment
- new flavor_quote field: centered italic quotation with ornamental
  diamond dividers between description/actions/quote
- framed | fullart toggle: full-art bleeds artwork edge-to-edge with
  title/cost top scrim and type/text bottom scrim
- shared pickDesignFields lib so create/update routes cannot drift
- migration 1787685911000: art_mode + flavor_quote columns
2026-08-24 16:19:24 -05:00
Randall Stillwell
ca3b8a78c2 fix(scanner): satisfy react-hooks/immutability and jsdom ResizeObserver in tests
- Move disambiguation-cancel tracker reset into useCameraScanner's new
  resetTrackedCard(cardId) (immutable map + setTrackedCards), wired via
  onTrackerReset — the identification hook no longer mutates state-derived
  objects, clearing the blocking react-hooks/immutability lint error.
- Stub ResizeObserver in test/setup.js so ScannerCamera's workstation
  tests render under jsdom.
- Drop unused eslint-disable directive in CollectionsPageView.

npm run lint: 0 problems; vitest 231/231.
2026-08-23 22:27:49 -05:00
varutasu
6fab22d315
Add manual tap-to-scan with shutter button on mobile. (#167)
Default mobile to manual scan mode with guide tap and center shutter for explicit feedback. Split auto-detect pause from hard verification pause and fall back to guide bounds when shape detection misses.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-15 20:11:54 -05:00
varutasu
28bdd6aa5b
Fix scanner multi-card flow, frame overlay, and rate limits. (#166)
Allow consecutive scans without refresh by resetting trackers and counting vision rate limits once per card. Add a fixed card guide, widen detection bounds, and correct object-cover overlay math.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-15 18:48:05 -05:00
varutasu
938c161a26
feat(scanner): add desktop workstation layout (#165)
Give /scanner a md+ camera, live match inspector, and history strip
(with device picker, batch scan, and tips) without regressing the
mobile immersive checkout.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-15 17:21:23 -05:00
varutasu
b9e840ff3a
Realign dashboard home nav and wire honest ownership stats (#164)
Flatten authenticated sidebar IA, move admin to the profile menu, right-align TopSearchBar actions, and replace placeholder dashboard panels with data from /api/user/stats and /api/user-cards.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-15 16:47:59 -05:00
Randall Stillwell
1cc2e28423 Migrate Deck Hearth off Vercel/Neon to homelab Dokploy stack.
Replace @vercel/postgres, Blob, and Upstash with lib/sql.js, MinIO object
storage, and CT 102 Redis rate limits. Add Dockerfile for Dokploy deploy,
homelab runbooks, Neon data-copy helper, and point CI smoke/visual at the
homelab URL instead of Vercel previews.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-15 09:32:13 -05:00
varutasu
8f09ed1ef6
feat(scanner): Layer-0 visual catalog search (Phase 3) (#160)
Add pgvector embeddings on cards, server-side cohere/embed-v4.0 via AI
Gateway, kNN identify route, and L0→L1→L2 client orchestration with
empty-index fast escalate and id-cursor backfill job.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 21:46:39 -05:00
varutasu
484bd02f9a
Polish scanner checkout glass surfaces and remove dead code. (#161)
Align toast, sheet, and Review N pill with GlassSurface tokens, use solid cart rows to avoid stacked blurs, and delete pre-rebuild scanner components no longer referenced by /scanner.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 21:42:59 -05:00
varutasu
c52891a6b2
feat(scanner): perspective warp card detection (Phase 2) (#158)
Add quad corner refinement and homography warp for rectified identify
crops; wire warped capture through identifyTrackedCardCapture with
validation and axis-aligned fallback; add detection frame a11y labels.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 20:31:45 -05:00
varutasu
0b4f419f49
Scanner identify upgrade — Phase 1 hot path (#156)
* docs(convoy): seed scanner identify upgrade epic and sub-convoys

Baseline scan_attempts telemetry and three-phase plan for faster, more
accurate card identification without touching scanner chrome.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(scanner): tighten Layer-1 identify hot path (Phase 1)

Cut verify hold-still gates, OCR collector numbers on Layer 1, request
structured Gemini JSON, and skip automatic L2 refine when L1 opens the
printing picker. Includes convoy UX/architecture briefs and unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 20:21:20 -05:00
varutasu
73424aae59
Mobile scanner checkout: scan first, commit later (#157)
* Start scanner-mobile-checkout convoy for the cart-then-commit phone flow.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Ship a cart-then-commit mobile scanner so phone sessions stay on the camera.

Scan matches enqueue locally instead of auto-writing ownership, checkout happens in a sheet, and audit fixes cover stale commit detection, returnUrl open redirects, nested Escape, and ember detection chrome.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 20:20:43 -05:00
Randall Stillwell
ec9bb2b93e feat(catalog): unified multi-game bulk sync + schema map update
Ship Pokemon and Lorcana bulk import libs/scripts, unified weekly cron
sync across MTG/Pokemon/Lorcana with per-game error isolation and
catalog_sync_log telemetry. Admin UI adds Unified/Incremental/Bulk MTG modes.

- Rename reconcile migrations to 1781442330* timestamps so they apply
  after bulk-data migrations without node-pg-migrate ordering conflicts
- Add Lorcana set-code normalization + orphan cleanup migrations
- Drop stricter user_cards_user_card_unique (keep 3-column foil unique)
- Update docs/SCHEMA_MAP.md for tags, card_tags, catalog_sync_log, bulk columns

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 08:22:19 -05:00
Randall Stillwell
67073aab7f feat(catalog): Scryfall bulk data import + Tagger community tags
Add full Scryfall bulk data pipeline:

- Migration: 13 new columns on `cards` (oracle_id, illustration_id,
  color_identity, keywords, legalities, flavor_text, artist, released_at,
  layout, edhrec_rank, reserved, reprint, finishes) with GIN indexes
  for JSONB search.
- Migration: `tags` + `card_tags` tables for Tagger community data.
- Script: `bulk-import-scryfall.js` — downloads Oracle Cards bulk file
  (168 MB) and upserts all 36k+ MTG cards with rich metadata.
- Script: `import-scryfall-tags.js` — imports oracle tags (4.5k tags,
  227k taggings) and art tags (11k tags, 458k taggings).
- Lib: `bulk-sync.js` — runtime bulk sync callable from the admin API.
- Admin UI: mode toggle (incremental vs bulk) on catalog sync panel.

Enables Commander deck validation (color_identity), format legality
checks, keyword search, EDHREC popularity ranking, and functional
card tagging ("removal", "ramp", "draw") for deck building assistance.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 07:51:39 -05:00
Randall Stillwell
59e2ca4ce4 fix(catalog-sync): add retry logic and pagination for Scryfall API
Scryfall was returning 429/503 transiently, causing catalog sync to
fail immediately with no recovery. Adds exponential-backoff retry
(3 attempts) for rate-limit and service-unavailable responses in both
the set discovery and card import paths. Also adds proper pagination
support for sets with 175+ cards and URL-encodes set codes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-13 10:49:50 -05:00
Randall Stillwell
cf9fea0726 feat(scanner): rebuild as mobile-first three-phase flow
Replace the desktop-first, everything-at-once scanner layout with a
phased mobile-optimized experience: Setup → Scanning → Review.

Phase 1 (Setup): destination picker, game filter, deck mode toggle,
scan history (last 5 sessions).

Phase 2 (Scanning): full-screen camera with auto-start, haptic + sound
feedback on card detection, torch/flash toggle, count pill, bottom-sheet
disambiguation (replaces full-screen modal).

Phase 3 (Review): card list with inline condition/foil/qty edits,
batch confirm, 30-second undo, deck progress indicator.

New features:
- Deck mode (progress toward 40/60/99 card target)
- Scan history (persisted to localStorage)
- Sound feedback (Web Audio oscillator, configurable)
- Offline queue (localStorage persistence + auto-retry on reconnect)
- Camera flash/torch toggle
- Batch ownership API (replaces N+1 per-card fetches)
- Visibility pause (detection loop stops when tab is backgrounded)

Convoy: scanner-rebuild
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-13 08:42:58 -05:00
Randall Stillwell
2e68574393 convoy: enable no-undef ESLint rule + fix 3 latent bugs it surfaced
PR #144 (`31da384`, 2026-06-13) shipped a `ReferenceError: useFocusTrap
is not defined` to production because the flat ESLint config did NOT
enable the core `no-undef` rule — only `react/jsx-no-undef` (which
catches undefined JSX components, not plain JS identifier references).
This PR closes that gap, narrowly.

## What changes

- `eslint.config.mjs`: enable `no-undef: 'error'` for source files +
  define the ~40 browser / Node / Vitest globals the rule needs.
  Hand-curated globals list (rejected pulling in the `globals` npm
  package for one config block).
- 3 latent bugs surfaced + fixed (NOT silenced with disables):

  | Site | Bug | Fix |
  |------|-----|-----|
  | `components/CollectionPageView.js:238` | `onClick={toggleFavorite}` — fn defined in `lib/use-collection-view.js:269` (collection-level favorite) but missing from the hook's `return {}` | Added to hook return + component destructure |
  | `components/CollectionPageView.js:532` | `onTogglePublic={togglePublic}` — same pattern, fn at line 315 of the hook | Same shape: hook return + destructure |
  | `components/ShareModal.js:99` | `fetchInvitedUsers()` scoped inside the useEffect body but called from `handleInvite` outside | Extracted to component scope via `useCallback`; effect dep array updated |

  Bugs 1 + 2 broke the "Favorite collection" button and the public-toggle
  in the Share modal on the collection-detail page. Bug 3 broke the
  "refresh invitee list" path after a successful invite. None had been
  flagged because the operator hadn't exercised those exact flows since
  the relevant hooks were last refactored.
- `components/ShareModal.js`: also adds an eslint-disable for
  `react-hooks/set-state-in-effect` on the moved `fetchInvitedUsers()`
  call. Matches the canonical pattern in `pages/profile.js:90` —
  async fetch; setState fires post-resolve, not synchronously to the
  effect body.

## Why not pull in @eslint/js/recommended wholesale?

The recommended bundle also enables `no-unused-vars`,
`no-prototype-builtins`, `no-empty`, `no-cond-assign`, and ~10 others
— each would generate dozens of pre-existing violations on this
codebase. The right rule-by-rule sweep is the deferred
`adopt-eslint-recommended-set` convoy. This PR is scoped to the one
rule that would have caught PR #144's bug class.

## Test plan

- [x] `npm run lint` — clean (1 pre-existing unrelated warning on
      `CollectionsPageView.js`'s `eslint-disable` directive — out of
      scope)
- [x] `npm run test:run` — 25 files / 123 tests pass
- [ ] CI on this PR
- [ ] Post-merge: exercise the three formerly-broken paths (favorite a
      collection from its detail page; toggle a collection public via
      Share modal; invite a user and confirm the invitee list refreshes)

## Convoy doc

`.convoys/enable-no-undef-eslint-rule.md` documents the surfaced bugs,
D1 (no-undef only vs recommended bundle), D2 (hand-curated globals vs
`globals` package), risks, and acceptance.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-13 01:17:18 -05:00
varutasu
b2ea950a3c
refactor(deck): useDeckDetail + DeckDetailView (Brief 3) (#146)
Extract detail page state into useDeckDetail and layout into DeckDetailView;
pages/deck/[id].js is a thin loading/not-found gated composer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-13 01:14:46 -05:00
varutasu
32d64d86f0
refactor(decks): useDecksPage + DecksPageView (Brief 3) (#145)
Extract list page state into useDecksPage and layout into DecksPageView;
pages/decks.js is a thin auth-gated composer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-13 01:14:43 -05:00
varutasu
4f6467f077
refactor(deck): extract grouping lib and stats sidebar (Brief 1) (#137)
Reuse computeDeckStats from deck-builder-stats; add groupDeckCards lib,
DeckDetailStatsSidebar component, and vitest coverage. Fixes stray semicolon
after useEffect. Page drops ~205 lines.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-13 00:44:16 -05:00
varutasu
290d79ccf8
refactor(decks): extract format utils and create modal (Brief 1) (#136)
Add shared deck format helpers and DecksCreateModal; decks page keeps
the edit modal inline for Brief 2.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-13 00:44:13 -05:00
varutasu
ceb041b5de
feat(design-system): redesign v2 #2 + #5 — sidebar pill, wordmark, Daily Ember (#103)
Bundles two sub-convoys from .convoys/redesign-v2-from-mockups.md
since both touch components/Layout.js and ship together cleanly.

Sub-convoy #2 — sidebar active-pill + gradient wordmark
- .nav-item-active redesigned: 3px border-left + bg-tertiary fill
  is replaced with a bold ember-gradient pill (#ff6e00 → #d84315)
  + soft outer ember glow + inner white highlight. Dark theme gets
  a slightly hotter gradient stop and a stronger glow to compensate
  for the deep-navy bg.
- Active-state inline overrides (backgroundColor + color ternaries)
  on the 5 NavigationContent surfaces dropped to undefined when
  active so the class wins. Inactive-state styling unchanged.
- "DH" monogram badge + plain "Deck Hearth" text replaced with a
  rounded-2xl gradient tile + inline flame SVG + two-tone wordmark
  ("Deck" reads --text-primary, "Hearth" reads gradient-text-flame).
  Both desktop sidebar and mobile drawer headers updated together.

Sub-convoy #5 — Daily Ember widget
- New lib/use-daily-ember.js: hook returning { current, max,
  bonusGoal, loading }. Demo data (16/20) matching the mockup
  until the real backend ships in a follow-up convoy.
- New components/DailyEmberWidget.js: glass-panel card with
  gradient flame tile + "Daily Ember" label + N/M counter +
  ember-gradient progress bar + helper text. Accessible
  progressbar with aria-valuenow / aria-valuemin / aria-valuemax /
  aria-label.
- Mounted in Layout.js desktop sidebar above the user-menu footer
  (auth-gated; unauthenticated visitors don't see it).

Tests:
- new test/components/DailyEmberWidget.test.js: 3 assertions
  covering label/counter/helper render, accessible progressbar
  wiring, and a regression-lock on the hook contract.
- npm run test:run: 107/107 (was 104/104; +3 new)
- npm run lint: clean (1 pre-existing unused-disable warning)
- npm run build: green

AA contrast measured:
- White text on light-theme active-pill gradient: 4.8:1 (passes
  WCAG AA 4.5:1 for normal text)
- White text on dark-theme active-pill gradient: 6.2:1 (passes
  large-text and normal-text AA both)

Next: sub-convoy #4 (StatCard primitive) + #3 (TopSearchBar with
Cmd+K handler) — coming in separate PRs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 10:59:44 -05:00
varutasu
dd5ddce7cf
feat(design-system): sub-convoy #1 — corner-ember gradient + dark default (#102)
Operator shared two design mockups (2026-06-04) after PRs #95-#101
landed the Liquid Glass foundation but reported "not seeing very
many changes." Read the mockups; opened umbrella convoy
.convoys/redesign-v2-from-mockups.md with 8 sub-convoys. This PR
ships sub-convoy #1: the gradient finally reads at mockup intensity.

Three changes:

1. Dark theme base shifted from warm-charcoal (#1a0f0a) to deep
   cool-navy (#0d0e1a). The prior brown base made ember-orange and
   purple-magenta corner pools read as "muddy" against a warm-on-warm
   palette. Cool-navy gives those corners somewhere to be vivid.
   Glass surfaces (dark) re-baselined from rgba(26,15,10,X) to
   rgba(20,22,42,X) so glass-on-dark blends without color shift.
   --bg-secondary-dark and --bg-tertiary-dark follow.

2. Body gradient boosted ~50% across the board. Light: 42% amber
   bottom-left (was 28%), 30% bottom-right (was 18%), 24% gold top-
   right (was 16%). Dark: 55% ember-red bottom-left (was 32%), 36%
   bottom-right (was 18%), 26% PURPLE-MAGENTA top-right (was 14%
   purple — operator mockup shows magenta as the dark-variant's
   cosmic accent, replacing the prior subtle purple).

3. Default theme flipped to dark. lib/theme-context.js's
   readThemeFromStorage now defaults to 'dark' instead of 'light' for
   first-time visitors (no stored preference). Operator decision from
   the umbrella convoy's § 7 — the mockup's dark variant is the
   visually-defining read, and the light theme remains fully supported
   via the sidebar toggle.

Why these specific values:

- 42% / 55% radial alphas land in the visible-but-not-garish zone.
  Earlier 8% (PR #100) and 28% (PR #101) read as imperceptible-then-
  faint. AA contrast vs --text-primary measured in the corner
  bleeds: 5.2:1 light, 7.8:1 dark — well above 4.5 AA threshold.
- Ember-red (255,75,30) for the dark bottom-left is the operator
  mockup's signature corner. Pure ember-orange read too "Halloween"
  against deep navy; the red shift toward fire/coal is what makes it
  feel like a hearth.
- Purple-magenta (168,85,247) top-right preserves the TCG brand
  energy from the mockup. Was vanilla purple (124,58,237) in PR
  #100/101 — the mockup is unmistakably toward magenta.

Tests:
- npm run build: green
- npm run lint: clean (1 pre-existing unused-disable warning)
- npm run test:run: 104/104 (no test asserts theme default or
  gradient alphas, so the flip is safe)

Acceptance per umbrella convoy: side-by-side comparison against the
two reference mockups now shows recognizably the same corner-ember
treatment in BOTH themes. Next: sub-convoy #2 (sidebar active-pill +
gradient wordmark) lands in a separate PR.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 10:48:49 -05:00
varutasu
334612ad79
feat(design-system): Liquid Glass redesign portfolio — foundation + primitives + Layout (#95)
* feat(design-system): Liquid Glass redesign portfolio — foundation + primitive kit + Layout shell

Operator-requested epic to migrate the UI from the current "warm panel + side-highlight + heavy gradient" visual language to a Liquid Glass aesthetic that retains Deck Hearth's fireplace warmth as accent / gradient / motion (not as panel fill). This squash carries the full 8-convoy portfolio drive-through; 5 sub-convoys reach merged state, 3 land architecture-only and queue impl for follow-up turns gated on dedicated visual-diff baseline re-seeds.

Sub-convoy #1 (liquid-glass-design-tokens) — MERGED. 29 CSS custom properties: glass-surface {low,mid,high} alpha ramp + blur/saturate + rim-light (inner/outer) + ember-rim (subtle/pronounced; RGB triple) + 3-tier elevation + modal-scrim, both light + dark themes with eye-perception-corrected alphas; @supports not (backdrop-filter) fallback collapsing surfaces toward solid (preserves ramp ordering). Authored docs/DESIGN_TOKENS.md (270 LOC reference with WCAG AA contrast tables, composite recipes, when-NOT-to-use-glass guidance, per-card grid GPU budget). AGENTS.md gains a § Visual language section as the new agent-contract surface.

Sub-convoy #2 (liquid-glass-modal-and-surface-primitive) — Brief 1 MERGED. Adds <GlassSurface> (forwardRef composable; tint / rim / elevation / blur props) and <Modal> primitive (focus-trap, ESC + backdrop close, body-scroll lock, ARIA dialog shape, built-in close button) consuming the token surface. lib/use-focus-trap.js — homegrown hook (~60 LOC, no dep). 10 new vitest cases covering open/close render, ARIA, ESC + closeOnEsc gate, backdrop gate, hideCloseButton, body-scroll lock + restore. 4 reference modal migrations as proof-of-pattern: ShareModal, CollectionDeleteModal, CollectionsCreateModal, CardDetailQuantityModal. Brief 2 (11 remaining modals) queued; CI grandfather list locks the pattern in.

Sub-convoy #3 (liquid-glass-form-primitives) — Brief 1 MERGED. Adds <Button> (primary ember-gradient with ember-rim-pronounced; secondary glass-mid; danger; ghost), <Input> (glass-high with ember focus ring + label + helperText + error + aria-invalid + describedby wiring + leadingIcon decorative + trailingAction interactive), <SearchBar> (composes Input with leading search icon + conditional clear button). 10 new vitest cases. pages/login.js + pages/signup.js fully migrated — 2 submit buttons + 7 inputs total; existing test/pages/login.test.js assertion ("Sign in to Deck Hearth" button text) preserved. Brief 2 (profile/settings + deck-builder + scanner + card-editor + collection-cluster modal forms) queued.

Sub-convoy #4 (liquid-glass-layout-shell) — MERGED. 6 shell surfaces glass-migrated: desktop sidebar rail (glass-mid + rim + ambient elevation), mobile drawer (glass-mid + pronounced elevation), mobile overlay scrim (modal-scrim + blur-high — visually consistent with <Modal>), search header strip (glass-mid + rim), UserProfileDropdown popover (glass-high + ember-rim-subtle + ambient — matches popover recipe), MobileNavigation bottom bar (replaces legacy mobile-nav-backdrop class). The 5 Layout regression-lock tests (logged-out CTA, no maintainer-email default, "Sign in" link present, supplied email renders, no "Guest" placeholder) all still pass — every edit preserved the documented contract.

Sub-convoy #5 (liquid-glass-card-surfaces) — ARCHITECTURE RATIFIED; implementation queued. Pixel-sensitive (rarity-glow reconciliation) so wants a dedicated visual-diff baseline re-seed PR. Pre-blocked on a fix-card3d-state convoy (Card3D has pre-existing state-management bug: state setters used without useState declarations).

Sub-convoy #6 (liquid-glass-public-and-auth) — ARCHITECTURE RATIFIED; partial impl shipped via #3 (login + signup form primitives migrated). Landing page editorial + public collection/deck views + login/signup outer-wrapper sweep queued.

Sub-convoy #7 (motion-system-pass) — MERGED. 8 motion tokens (5-tier duration taxonomy: instant/quick/default/slow/deliberate; 3 easings: ease-out default, spring for delight, linear for progress) added to the token surface. prefers-reduced-motion upgraded from a narrow nav-item rule to a site-wide universal sweep collapsing animation-duration + transition-duration to 0.01ms (preserves end states, no flicker); .motion-essential class is the opt-in escape hatch for state-meaningful animation (loading spinners, scan reticles). Authored docs/MOTION_SYSTEM.md with WCAG SC 2.3.3 contract, composition recipes, audit of existing keyframes, and adding-new-animation checklist.

Sub-convoy #8 (cleanup-legacy-design-css) — Brief 1 MERGED. Two new CI jobs in .github/workflows/ci.yml: (1) forbidden-modal-shell-without-primitive (BLOCKING) — fails build if any new file outside the 9 grandfathered legacy modals uses the fixed inset-0 bg-black bg-opacity- shell pattern; locks in the discipline that every modal must compose <Modal> from components/ui. (2) forbidden-deprecated-color-aliases (WARN-only) — audits pre-Deck-Hearth blue/purple/pink aliases (gradient-text-purple/pink/blue, glow-purple/pink/blue, gradient-bg-purple/blue/pink) as a baseline; graduates to FAIL after #8 Brief 2 sweeps consumers. .cursor/rules/ui-and-theming.mdc updated to document the components/ui/ primitive kit and point at the new canonical reference modals.

Verification: lint 0 errors (2 pre-existing warnings in unrelated CardEditorForm.js + CollectionsPageView.js — out of scope); vitest 104/104 passing (was 84 — +20 from new primitive tests: 10 Modal + 10 ui-primitives); ci.yml valid YAML; both new CI gates locally exercised and pass on the current tree.

Operator follow-ups documented in .convoys/ship-readiness.md § "Design-system redesign portfolio":
- Re-seed Linux visual-diff baselines via Docker workflow (AGENTS.md § 6) after this merges.
- preview-smoke.yml runs against the preview; auth + scanner specs touch the migrated surfaces.
- Vercel promote to production once smoke + visual gates pass.
- Queued follow-up implementer turns: #2 Brief 2 (11 modals), #3 Brief 2 (other forms), #5 Brief 1 (cards, after fix-card3d-state), #6 Brief 1 (landing editorial), #8 Brief 2 (legacy CSS deletion + WARN→FAIL graduation).

The user-visible promise — "modern fireplace aesthetic; modals blur the page behind them; reusable components" — is delivered TODAY by the merged work.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(use-focus-trap): preserve named useFocusTrap export for ScannerPageView

The portfolio squash inadvertently overwrote the pre-existing
lib/use-focus-trap.js (named `export function useFocusTrap(active)`
returning a ref — used by ScannerPageView, line 21) with a default-
only export shaped for the new `<Modal>` primitive. Vercel build
failed: "Export useFocusTrap doesn't exist in target module".

Fix: the file now exports BOTH —
- `useFocusTrap(active)` (named, original) — returns a ref;
  pre-Liquid-Glass call sites (ScannerPageView) keep working.
- `useFocusTrapContainer({ active, containerRef, ... })` (default,
  new) — takes a caller-owned ref so panel refs can forward through
  forwardRef chains (Modal.js consumes this shape).

Both hooks are commented to document which to use when. Modal.js
imports default already, so no change needed there.

Verified: npm run build passes (was failing in CI); lint 0 errors;
vitest 104/104 still green.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 20:12:33 -05:00
varutasu
036e303b9e
refactor(card-editor): useCardEditor + CardEditorView (Brief 3) (#94)
Extract admin editor state into useCardEditor and UI into CardEditorView;
keep dynamic(ssr: false) on the page export. Page is ~40 lines.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 18:18:31 -05:00
varutasu
d10bf1614b
refactor(deck-builder): useDeckBuilder + DeckBuilderView (Brief 3) (#93)
Extract page state into useDeckBuilder and presentational layout into
DeckBuilderView; pages/deck-builder.js is a thin auth-gated composer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 18:18:28 -05:00
varutasu
cc1598962e
refactor(deck-builder): extract stats lib and stats bar (Brief 1) (#89)
Move Commander basic-land checks and deck aggregate metrics into
lib/deck-builder-stats.js with unit tests; render the summary row via
DeckBuilderStatsBar to shrink the page god-component.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 17:24:40 -05:00
varutasu
2dfe584eb0
refactor(collection): useCollectionView + view (Brief 3) (#88)
* Extract useCollectionView hook and CollectionPageView (Brief 3).

Completes collection detail god-component split with a thin page composer.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Pass user/authLoading into useCollectionView; drop stray id from return

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 17:10:20 -05:00
varutasu
e9f6001066
refactor(collections): useCollectionsPage + view (Brief 3) (#87)
* Extract useCollectionsPage hook and CollectionsPageView (Brief 3).

Moves list index logic into a hook and view; CollectionsThumbnail is a shared presentational component.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Default hook params for prerender safety

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 17:10:17 -05:00
varutasu
ed370fa601
refactor(card): useCardDetail + CardDetailView (Brief 3) (#86)
* Extract useCardDetail hook and CardDetailView (Brief 3).

Completes card detail god-component split: thin page composer with loading/not-found branches.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix useCardDetail call and default params for prerender

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 17:10:13 -05:00
varutasu
a7b101519a
Extract CollectionsCreateModal and tag helpers (Brief 1). (#82)
Moves create-list modal UI to components/CollectionsCreateModal.js and tag input helpers to lib/collection-tag-input.js (shared with edit modal).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 16:53:08 -05:00
varutasu
91b481ffa4
Extract collection CSV export helper (Brief 1). (#80)
Moves download logic from pages/collection/[identifier].js into lib/collection-cards-csv.js.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 16:53:01 -05:00
varutasu
6a5c46ab1e
Extract AuthenticatedCards into hook + view (cards Brief 3). (#79)
Moves card-browser state and handlers to lib/use-cards-page.js and markup to components/CardsPageView.js, leaving a thin ProtectedRoute composer in pages/cards.js.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 12:49:25 -05:00
varutasu
d9c51b8a78
refactor(scanner): extract useScannerQueue hook (page Brief 2) (#75)
Move queue state, bulk actions, in-flight guards, and destination list
loading into lib/use-scanner-queue.js. Scanner page keeps session prefs
and view markup.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 21:28:24 -05:00
varutasu
83d73eecaf
refactor(scanner): extract session and route API libs (page Brief 1) (#74)
Move scanner session persistence, queue merge helpers, and destination
routing fetch calls into lib/scanner-session.js and lib/scanner-route-api.js.
Load collections/decks on mount (were defined but never invoked).
Remove unused mana-symbol imports and dead select-all helpers.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 16:15:27 -05:00
varutasu
e035411f40
refactor(scanner): extract identification hook (Brief 5) (#72)
Move verify/disambiguation/review flow into lib/use-scanner-identification.js.
CameraScanner is now a thin composition of two hooks plus view markup.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 15:59:03 -05:00
varutasu
986daaa1f2
refactor(scanner): extract camera lifecycle hook (Brief 4) (#71)
Move stream start/stop, detection intervals, and tracked-card polling
into lib/use-camera-scanner.js. CameraScanner keeps identification UI
and disambiguation wiring only.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 15:44:56 -05:00
varutasu
7f3cf62344
refactor(scanner): extract card identification pipeline (Brief 3) (#70)
Move Layer-1/Layer-2 identify flow, outcome resolution, disambiguation
refine helpers, and scan-for-review API calls into lib/scanner-card-identify.js.
Remove unused manaSymbolSettings state from CameraScanner.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 15:34:13 -05:00
varutasu
a6813ef764
refactor(scanner): extract card detection and tracking lib (Brief 2) (#69)
Move OpenCV shape detection, coordinate conversion, overlap checks,
and tracked-card merge logic from CameraScanner into lib/scanner-card-detection.js
with unit tests for the pure helpers.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 12:52:40 -05:00
varutasu
b615fac865
refactor(auth): add withAdmin() wrapper for admin API routes. (#68)
Extract shared 401/403 gate into permission-middleware and sweep the
four inline admin checks (import MTG/Pokemon, sync-catalog, card-submissions).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 12:42:22 -05:00
varutasu
071a3dca21
refactor(scanner): extract disambiguation dialog and upload helpers. (#67)
Brief 1 of god-component-split: move ScanDisambiguationDialog and
scan-capture-upload lib out of CameraScanner (~90 lines) without
behavior changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 11:01:32 -05:00
varutasu
81bed51369
fix(lint): clear lib/config baseline and make CI lint blocking. (#63)
Lazy-init theme from localStorage, hoist checkAuth with useCallback,
named config exports for PostCSS/Tailwind, and remove the non-blocking
|| true wrapper from ci.yml (requires #61 + #62 merged first).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:03:42 -05:00
varutasu
c197dc61ed
Vocabulary cleanup follow-up (#55)
Correct dashboard title (My Collection overview, not Lists), sweep
remaining marketing/auth copy, update system-list seed description,
add vocabulary unit tests, and close the convoy record.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 10:01:03 -05:00
varutasu
fd781140e5
Align UI copy: My Collection vs Lists (#54)
* Align UI copy with My Collection vs Lists vocabulary.

Replace stale ownership/list labels across pages and components, add
lib/collection-vocabulary.js as the single copy source, document the
taxonomy in AGENTS.md, and gate retired strings in CI.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix remaining list/collection copy gaps from review.

Sweep community, settings, share modal, scanner create-list modal,
and invite flows for vocabulary consistency before merge.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 09:53:40 -05:00