Commit graph

400 commits

Author SHA1 Message Date
8a5ec11573 Merge pull request 'perf(scanner): three high-impact speed optimizations' (#156) from perf/scanner-speed-optimizations into main
Some checks are pending
CI / Lint (push) Waiting to run
CI / Schema map up to date (push) Waiting to run
CI / Forbidden patterns (9 checks) (push) Waiting to run
CI / Migrations apply (node-pg-migrate) (push) Waiting to run
CI / Unit tests (vitest) (push) Waiting to run
2026-09-01 18:18:13 -04:00
Randall Stillwell
04504903bf perf(scanner): three high-impact speed optimizations
Some checks are pending
CI / Lint (pull_request) Waiting to run
CI / Schema map up to date (pull_request) Waiting to run
CI / Forbidden patterns (9 checks) (pull_request) Waiting to run
CI / Migrations apply (node-pg-migrate) (pull_request) Waiting to run
CI / Unit tests (vitest) (pull_request) Waiting to run
Convoy metrics gate / Require role-event telemetry on convoy PRs (pull_request) Waiting to run
PR Health rollup / Aggregate gate status (pull_request) Waiting to run
Preview smoke / Should run? (pull_request) Waiting to run
Preview smoke / Playwright smoke (pull_request) Blocked by required conditions
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
e5cb8569e5 Merge pull request 'feat(scanner): add debug instrumentation for vision pipeline timing' (#155) from feat/scanner-debug-mode into main
Some checks are pending
CI / Lint (push) Waiting to run
CI / Schema map up to date (push) Waiting to run
CI / Forbidden patterns (9 checks) (push) Waiting to run
CI / Migrations apply (node-pg-migrate) (push) Waiting to run
CI / Unit tests (vitest) (push) Waiting to run
2026-09-01 18:11:03 -04:00
Randall Stillwell
106bd9d592 feat(scanner): add debug instrumentation for vision pipeline timing
Some checks are pending
CI / Lint (pull_request) Waiting to run
CI / Schema map up to date (pull_request) Waiting to run
CI / Forbidden patterns (9 checks) (pull_request) Waiting to run
CI / Migrations apply (node-pg-migrate) (pull_request) Waiting to run
CI / Unit tests (vitest) (pull_request) Waiting to run
Convoy metrics gate / Require role-event telemetry on convoy PRs (pull_request) Waiting to run
PR Health rollup / Aggregate gate status (pull_request) Waiting to run
Preview smoke / Should run? (pull_request) Waiting to run
Preview smoke / Playwright smoke (pull_request) Blocked by required conditions
Visual diff / Should run? (pull_request) Waiting to run
Visual diff / Screenshot diff (pull_request) Blocked by required conditions
- 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
5b9a278ca2 feat(designer): inline symbol icons, frame textures, community sharing, print sheets
- {CODE} tokens in description/actions/flavor render as inline symbol
  icons (RichText), including inside cost pips
- custom frames gain an optional background texture (upload/replace/
  remove via /api/custom-frames/[id]/texture); renders behind panels
- custom games can be shared to the community (is_public): toggle in the
  game space, public listing at /community/games, read-only game view,
  /api/public/games endpoints (no auth, public rows only)
- /designer/print: multi-card print sheets on US Letter at 300dpi
  (63x88mm cards, 3x3 or 2x2, dashed cut guides, full-sheet PNG export)
- migration 1787711511000
2026-08-24 21:48:52 -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
421c5e5ee5 feat(designer): add card designer with starter frames, live preview, and PNG export
- custom_cards migration + CRUD API with catalog twin sync so designs
  appear in My Cards, lists, and decks via normal card joins
- artwork upload to MinIO under card-art/
- /designer page: form-driven live preview, 4 starter frames, PNG export
- /my-designs gallery with edit/delete
- Designer nav entry in sidebar + mobile drawer
2026-08-24 14:53:06 -05:00
Randall Stillwell
f2ba333daf docs: sweep README + AGENTS.md to post-homelab reality
- README: Dokploy/CT102 Postgres stack, lib/sql.js, six rate-limit
  classes over REDIS_URL, Playwright smoke+visual, MinIO/S3 vars,
  inline .env.local contract (no .env.example exists), deckhearth.git
  clone URL, project-structure refresh.
- AGENTS.md: infra banner for the Vercel/Neon -> homelab move, repo
  rename to stwl-labs/deckhearth, Data/Auth/Hosting overview bullets,
  DB-access convention re-pointed at lib/sql.js, Gotcha #12 rewritten
  for REDIS_URL + scan limiter (+ row in limiter table), test counts
  refreshed (231/231 across 43 files), §5 env contract, §7 marked
  legacy-pending-decommission with DOKPLOY_DEPLOY runbook pointer.
2026-08-23 22:34:54 -05:00
Randall Stillwell
95c2f6003b docs(convoy): flip shipped statuses (scanner-desktop-layout, reconcile-historical-add-scripts)
- scanner-desktop-layout: open -> shipped; adds As-shipped section
  (PR #165, squash 938c161, 2026-08-15) with post-merge follow-up refs.
- reconcile-historical-add-scripts: open -> shipped to match its
  existing As-shipped section and ship-readiness RESOLVED entry.
2026-08-23 22:27:54 -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
Randall Stillwell
fdf8f5ece4 Mark dashboard-home-realignment convoy shipped (PR #164).
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-15 16:48:07 -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
varutasu
0d52858bbd
docs: post-hoc B7 closure for reconcile-historical-add-scripts (#163)
Ships Brief 7 (documentation + verification runbook) of the
reconcile-historical-add-scripts convoy, ~2 months post-hoc. The
6 implementer briefs (B1-B6) landed 2026-06-14 to 2026-07-06 via
PRs #148, #149, #150, #151, #152, #153. This PR closes the loop:

- Brings the architect's parent convoy file + 6 brief files onto
  main (they only existed on the stale convoy/reconcile-historical-
  add-scripts branch, never merged)
- Adds § As-shipped to the parent convoy file documenting all 6
  squash SHAs + PR numbers + merge dates + the reservation-timestamp
  rename (1781000000001-006 → 1781442330001-006 in ec9bb2b, except
  B3 which kept its original) + the B6 shipped-as-tiny-migration
  deviation from the collapse-to-docs plan
- Fixes docs/SCHEMA_MAP.md § user_favorites (was stale
  (user_id, card_id); actual polymorphic (item_type, item_id) per
  B4's migration)
- Adds docs/MIGRATION_VERIFICATION_RUNBOOK.md — manual
  fresh-Neon-branch vs prod pg_dump diff runbook per architect D5
- Flips .convoys/ship-readiness.md entries:
  - reconcile-historical-add-scripts → RESOLVED
  - retire-graveyard-scripts-after-audit → UNBLOCKED
- Adds two new queued follow-ups surfaced by the architect:
  - unify-user-avatar-column (P3 — dual avatar column smell)
  - drop-dead-cards-columns (P3 — cards.quantity + cards.favorited)

No source-code changes. Docs only.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-15 16:31:37 -05:00
Randall Stillwell
c6a0225e54 Document post-ship scan_attempts telemetry and close Phase 1–2 convoys.
Record Aug 15 measurement showing Phases 1–2 missed success targets, Phase 3 code is live with 15% embedding backfill and zero L0 traffic yet.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-15 16:19:30 -05:00
Randall Stillwell
6ada83507c docs(dokploy): point deploy runbook at main branch
Homelab migration merged to main; Dokploy should track production there.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-15 09:59:17 -05:00
Randall Stillwell
636bcd35f5 Merge origin/main into homelab migration branch 2026-08-15 09:59:08 -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
7007eae3ba
fix(ci): Phase 3 post-merge CI gates (#162)
* fix(ci): allow card-embed.js and skip pgvector on non-superuser CI

Add lib/card-embed.js to the server-only LLM allowlist (forbidden-patterns
Check 3). Make the pgvector migration degrade gracefully when CT 102 CI
cannot CREATE EXTENSION vector so migrate up still passes.

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

* docs(convoy): post-ship metrics and operator checklist for Phase 3

Record Neon migration applied, scan_attempts snapshot, and backfill gate.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 21:53:34 -05:00
Randall Stillwell
a9d16d2e4d docs(convoy): post-ship metrics and operator checklist for Phase 3
Record Neon migration applied, scan_attempts snapshot, and backfill gate.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 21:48:58 -05:00
Randall Stillwell
b2c02cb45b fix(ci): allow card-embed.js and skip pgvector on non-superuser CI
Add lib/card-embed.js to the server-only LLM allowlist (forbidden-patterns
Check 3). Make the pgvector migration degrade gracefully when CT 102 CI
cannot CREATE EXTENSION vector so migrate up still passes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 21:48:25 -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
f6305f96f2
Stop CI migrate-up from indexing collections.visibility before the column exists. (#159)
B3 ran before the B2 column add on a fresh database. Move the index to a later migration so collaboration tables still apply and the index lands after visibility is present.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 20:32:10 -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
varutasu
c6c1364dd6
chore(agent-pipeline): sync 0.6.0/0.7.0 artifacts (#155)
* Sync agent pipeline artifacts to 0.6.0.

Add model routing defaults, L2 role updates, convoy telemetry, and manifest tracking without touching unrelated in-progress work.

* Record tcg-vault interactive sync (kept local L1/L3 customizations).

Update last_synced_at after reviewing behind/conflict artifacts; no overwrites applied.

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

* chore(agent-pipeline): sync 0.6.0 — implementer Mode 2 fix pass + UI designer

- role-implementer Mode 2 (fix pass) and role-reviewer hand-off
- role-ui-designer + model-routing updates from upstream 783e2a3
- manifest last_synced_at bumped

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

* chore(agent-pipeline): sync v0.7.0 model routing

Composer Standard for architect, Grok for audit fan-out, Sonnet 5 escalation path.

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

* chore(agent-pipeline): finish 0.6.0/0.7.0 sync on branch

Install security baseline, convoy planning, security-audit and ui-ux-pro-max
skills; align PR template and metrics docs with convoy-metrics-gate; refresh
manifest hashes while keeping tcg-vault-local L1/L3 customizations.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 18:56:01 -05:00
varutasu
15e02ee01b
feat(migrations): reconcile collaboration tables missed by initial-schema backfill (B3) (#151)
Captures the DDL half of scripts/add-collaboration-features.js that the
initial-schema backfill missed:

  - collection_permissions (collaboration roles + invite tokens)
  - collection_activity (audit-trail with JSONB details)
  - users.is_pending (column for invited-but-not-yet-accepted users)
  - 4 indexes for query hot-paths

lib/permission-middleware.js reads collection_permissions in
withCollectionPermission and writes collection_activity from
logCollectionActivity, so a fresh Neon branch onboarded by
`npm run setup-db` MUST land these tables. Prod was brought to this
shape by add-collaboration-features.js running historically; this
migration brings fresh envs to parity per
.convoys/reconcile-historical-add-scripts.md Brief outline -> B3.

Idempotent against fresh and existing envs:
CREATE TABLE IF NOT EXISTS / ADD COLUMN IF NOT EXISTS / CREATE INDEX
IF NOT EXISTS so re-application against any post-historical-script env
is a no-op except recording the pgmigrations row.

Owner-permission DML backfill is intentionally NOT captured - fresh
envs have no pre-existing collections needing backfill, and prod's
backfill is already applied.

down() is a hard stub matching the initial-schema pattern.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 11:27:46 -05:00
varutasu
71757faa90
chore: sync agent pipeline v0.6.0 (keep local L1/L3) (#154)
* Sync agent pipeline artifacts to 0.6.0.

Add model routing defaults, L2 role updates, convoy telemetry, and manifest tracking without touching unrelated in-progress work.

* Record tcg-vault interactive sync (kept local L1/L3 customizations).

Update last_synced_at after reviewing behind/conflict artifacts; no overwrites applied.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 11:27:33 -05:00
Randall Stillwell
8ee5e7bf05 chore(metrics): capture role event from multi-game-bulk-sync brief 2
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 15:34:04 -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
varutasu
40402eb287
feat(migrations): reconcile user_cards UNIQUE constraint per Option A (B6) (#149)
* feat(migrations): reconcile user_cards UNIQUE constraint per Option A (B6)

Convoy: reconcile-historical-add-scripts Brief 6.

Operator decision on Finding 1 (ratified 2026-06-14): Option A — keep
the canonical 3-col UNIQUE(user_id, card_id, is_foil) declared by
migrations/1779853647564_initial-schema.js. Drop / treat-as-no-op the
stricter 2-col UNIQUE(user_id, card_id) that the historical
scripts/fix-user-cards-constraints.js job would have installed. Foil
and non-foil copies of the same card are semantically separate rows.

Defensive idempotent shape; safe against all three prod states (fresh
Neon branch, long-lived env that never ran the script, long-lived env
that did run it). down() is a hard-stub throw — re-installing the
2-col constraint would forbid the foil distinction runtime code
relies on.

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

* fix(migrate): pre-check pg_constraint instead of catching duplicate_object

ADD CONSTRAINT UNIQUE creates a supporting index under the hood; when
the index name already exists from initial-schema's inline UNIQUE,
Postgres raises SQLSTATE 42P07 (duplicate_table), not 42710
(duplicate_object) — so the EXCEPTION block didn't catch it and CI's
Migrations apply gate failed with `relation
"user_cards_user_id_card_id_is_foil_key" already exists`.

Swap to a pg_constraint pre-check: bulletproof against both SQLSTATEs
without overreaching to WHEN OTHERS.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 08:18:22 -05:00
varutasu
ee7da9ac3a
feat(migrations): reconcile user profile missed by initial-schema backfill (B5) (#153)
Folds the union of DDL effects from `scripts/add-user-profile-columns.js`
(#8) and `scripts/add-user-profile-fields.js` (#9) into the migration
history so a brand-new Neon branch reaches structural parity with prod.

Captures:
- 16 ALTER TABLE users ADD COLUMN IF NOT EXISTS (first_name, last_name,
  username UNIQUE, profile_image_url, bio, avatar_url, favorite_games,
  collection_visibility, preferred_currency, cards_per_page, default_view,
  notifications_email, notifications_marketing, two_factor_enabled, theme,
  language). Per Finding 2, BOTH profile_image_url and avatar_url are kept
  (cleanup deferred to queued unify-user-avatar-column).
- 2 CREATE TABLE IF NOT EXISTS (user_settings, user_avatars) — FK to users
  with ON DELETE CASCADE; user_settings has UNIQUE(user_id, setting_key).
- 6 CREATE INDEX IF NOT EXISTS (idx_users_username, idx_users_email,
  idx_user_settings_user_id, idx_user_settings_key, idx_user_avatars_user_id,
  idx_user_avatars_active).
- 6 CHECK constraints wrapped in DO $$ EXCEPTION WHEN duplicate_object
  blocks (Postgres pre-15 has no ADD CONSTRAINT IF NOT EXISTS for CHECK):
  check_collection_visibility, check_preferred_currency, check_cards_per_page,
  check_default_view, check_theme, check_language.

Defaults-backfill DML from the historical script is intentionally NOT
replicated; column DEFAULTs handle fresh-env semantics and prod rows
already have the values from the historical run.

down() is a hard stub (rolling back would drop columns runtime code reads).

Convoy: reconcile-historical-add-scripts (Brief 5/7).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 08:18:20 -05:00
varutasu
e8619c5836
feat(migrations): reconcile collections columns missed by initial-schema backfill (B2) (#150)
Captures the `collections`-table DDL that historical scripts added to prod
but `migrations/1779853647564_initial-schema.js` did not capture:

  - `visibility VARCHAR(20) DEFAULT 'private'`  (scripts/add-collaboration-features.js, lines 15-21 — collections half only)
  - `tcg VARCHAR(50) DEFAULT 'MTG'`             (scripts/add-collaboration-features.js, lines 15-21 — collections half only)
  - `tags TEXT`                                 (scripts/add-collaboration-features.js, lines 15-21 — collections half only)
  - `slug VARCHAR(100) UNIQUE`                  (scripts/add-collection-slugs.js, lines 17-20)
  - `idx_collections_slug` UNIQUE INDEX         (scripts/add-collection-slugs.js, line 79)
  - `check_slug_format` CHECK constraint        (scripts/add-collection-slugs.js, line 91)
  - `image TEXT`                                (scripts/add-image-column.js, lines 14-17)

Idempotency (D2): every statement is `IF NOT EXISTS`-guarded
(ADD COLUMN IF NOT EXISTS, CREATE UNIQUE INDEX IF NOT EXISTS, plus a DO $$
pg_constraint guard for the CHECK since Postgres has no native IF NOT
EXISTS clause for named constraints). Safe against fresh, prod, and
re-apply.

Down() is a hard stub matching initial-schema style — these columns hold
visibility flags, slugs, tcg labels, tags, and images that production
collections rely on at every page render.

Out-of-scope per architect plan (B3 territory): collection_permissions,
collection_activity, users.is_pending, idx_collections_visibility, and
the 3 idx_collection_* indexes. Out-of-scope per architect plan (already
captured): is_system_collection (in 1780378340194).

Deferred DML: per-row slug backfill from `name` via
`lib/slug-utils.js::generateUniqueSlug`. Generating slugs on a fresh env
is moot (no pre-existing collections); operators of long-lived envs
already ran the backfill historically.

Static idempotency proof — grep confirms each B2 column/constraint is
defined exactly ONCE across all 8 existing migrations:

  $ grep -nE "(visibility|tcg|^.*tags TEXT|slug VARCHAR|^.*image TEXT|check_slug_format|idx_collections_slug)" migrations/*.js
  migrations/1781000000002_reconcile-collections-columns.js  (sole owner)

The `image_url` / `stock_image_url` matches in `1779853647564_initial-schema.js`
are on the `cards` table, not `collections`. The `tags` table created in
`1781440721350_add-tagger-tables.js` is a separate table from this
migration's `collections.tags` column.

Verification: node --check , npm run lint  (0 errors, baseline 1
unrelated warning), npm run test:run  (131/131). Live Neon-branch
verification deferred to operator runbook (D5 of the convoy plan).

Convoy: reconcile-historical-add-scripts
Brief: B2
Pre-assigned timestamp: 1781000000002

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 08:18:05 -05:00
varutasu
ef2cfb8547
feat(migrations): reconcile favorites system missed by initial-schema backfill (B4) (#152)
Captures the DDL effects of scripts/add-favorites-system.js (a historical
no-go-zone script) so a fresh Neon branch onboarded via npm run setup-db
has the same user_favorites table + 4 indexes that prod has via the
historical script. Brings fresh envs to parity with prod for the
favorites surface used by pages/api/favorites.js.

Shape matches the historical script and the runtime API verbatim:
  - user_favorites(id, user_id FK CASCADE, item_type VARCHAR(50),
    item_id INTEGER, created_at, UNIQUE(user_id, item_type, item_id))
  - idx_user_favorites_user_id / _item_type / _item_id / _user_type

CREATE TABLE / CREATE INDEX guarded with IF NOT EXISTS per convoy
decision D2 — re-running against any env where the historical script
already ran is a documented no-op (only the pgmigrations row is new).

down() is a hard stub: rolling back would drop user_favorites and every
row in it; removal deserves its own scoped convoy.

Part of .convoys/reconcile-historical-add-scripts (commit 22ebef2),
Brief 4 of 7. Base PR is main, not the parent convoy branch, per the
parallel-implementer dispatch pattern.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 08:11:26 -05:00
varutasu
a35ce01ba0
feat(migrations): reconcile cards columns missed by initial-schema backfill (B1) (#148)
Folds `scripts/add-card-columns.js` into the migration history as B1 of
the `reconcile-historical-add-scripts` convoy (architect plan at
commit 22ebef2). Adds two columns to `cards` that the initial-schema
backfill (1779853647564) did not capture in its bootstrap CREATE TABLE:

  - cards.quantity   INTEGER  DEFAULT 0
  - cards.favorited  BOOLEAN  DEFAULT false

Both columns exist in every long-lived env (the historical script ran
pre-migration-tool) but were missing from fresh-env onboarding via
`npm run setup-db` until now. They are flagged "Unused" in
docs/SCHEMA_MAP.md § "Known schema smells" #3; the follow-up
`drop-dead-cards-columns` convoy will retire them once a query-trace
audit confirms zero readers. Reproduced verbatim here to bring fresh
envs to prod-parity per the convoy's D3 ratification.

Idempotency (D2): both statements use ADD COLUMN IF NOT EXISTS, so
the migration is safe to run against fresh Neon branches, long-lived
prod envs where add-card-columns.js already ran, or re-applications.
Matches the raw `pgm.sql()` style of `1779853647564_initial-schema.js`.
`down()` is a hard-stub throw consistent with the rest of the
migration corpus's reconciliation/destructive guards.

Static idempotency proof — the `cards` CREATE TABLE block in
initial-schema (lines 45-69) does NOT contain `quantity` or
`favorited`; the three `quantity` hits in that file at lines 76, 103,
127 are on `user_cards`, `collection_cards`, and `deck_cards`. No
other migration mentions either column:

  $ rg -n "quantity|favorited" migrations/
  migrations/1779853647564_initial-schema.js:76:      quantity INTEGER DEFAULT 1,
  migrations/1779853647564_initial-schema.js:103:      quantity INTEGER DEFAULT 1,
  migrations/1779853647564_initial-schema.js:127:      quantity INTEGER DEFAULT 1,

PR #32's NEW post-architect migration `1781440700404_add-scryfall-bulk-columns.js`
adds 13 unrelated Scryfall bulk columns (oracle_id, illustration_id,
color_identity, keywords, legalities, flavor_text, artist, released_at,
layout, edhrec_rank, reserved, reprint, finishes) — verified to not
include quantity/favorited; no scope reduction required.

Verification:
  - `node --check migrations/1781000000001_reconcile-cards-columns.js` → exit 0
  - `npm run lint` → 0 errors, 1 pre-existing warning on main
    (components/CollectionsPageView.js, unrelated to this change)
  - `npm run test:run` → 131/131 tests pass across 26 files
  - End-to-end `npm run migrate up` against a fresh Neon branch:
    deferred to operator post-merge verification per D5 (D5 runbook
    lives in .convoys/reconcile-historical-add-scripts.md § Verification plan)

Refs: - Architect plan: .convoys/reconcile-historical-add-scripts.md (commit 22ebef2)
  - Historical script (no-go-zone, not edited): scripts/add-card-columns.js
  - SCHEMA_MAP smell entry: docs/SCHEMA_MAP.md § "Known schema smells" #3

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 08:11:08 -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
237870c17e convoy: render-test regression-lock for ScanDisambiguationDialog (PR #144)
PR #144 (`31da384`, 2026-06-13) shipped a runtime
`ReferenceError: useFocusTrap is not defined` to production because
the component called the hook without importing it. The sibling
`enable-no-undef-eslint-rule` convoy closes that bug class at LINT
time. This PR locks the same regression at RENDER time so the bug
would still fail CI even if the lint rule were dropped or disabled.

## What changes

- `test/components/ScanDisambiguationDialog.test.js` — 8 tests:

  1. `renders without crashing (PR #144 regression-lock)` — the
     direct lock-in. Mutation-tested: commenting out the
     `useFocusTrap` import causes all 8 tests to fail with the same
     `ReferenceError` shape that hit prod.
  2. `returns null when disambiguation is falsy`
  3. ARIA shape (`role`, `aria-modal`, `aria-labelledby`)
  4. One button per candidate with accessible labels
  5. `onPick` callback receives the selected candidate
  6. Vision-hint branch renders when provided
  7. Submitting state disables the "send for review" button
  8. `onCancel` callback fires on Cancel click

## Why vitest + jsdom and not Playwright smoke

| Path | Catches PR #144 | Setup | Runtime |
|------|-----------------|-------|---------|
| Playwright smoke | ✓ if disambiguation mounts in the smoke run | High (auth bypass, stable multi-candidate fixture image) | ~10s + browser |
| Vitest render | ✓ directly — render-throw → test fail | Low | <100ms |

Re-scoped the queued `scanner-disambiguation-smoke-test` task to the
vitest shape because a render test catches the exact same bug class
at 1/100th the cost and matches the existing `test/components/*.test.js`
pattern (`Modal.test.js`, `ScannedCardItem.test.js`, etc.). A Playwright
disambiguation smoke is still useful as integration-layer coverage and
is queued as `scanner-disambiguation-playwright-smoke`.

## Verification

- [x] `npm run test:run` — 26 files / 131 tests pass (up from 25/123)
- [x] Mutation test: with `useFocusTrap` import commented out, all 8
      tests fail with `ReferenceError`. With import restored, all pass.

## Test plan

- [ ] CI on this PR green
- [ ] Squash + merge
- [ ] Smoke test post-merge: scan a card that triggers disambiguation
      in prod and confirm no console errors (the original PR #144 bug
      shape)

## Convoy doc

`.convoys/scanner-disambiguation-render-test.md` documents D1 (cover
the early-return branch explicitly), D2 (`fireEvent` not `userEvent`),
D3 (do NOT mock `useFocusTrap` — the missing-hook is exactly what
we're locking), and the two queued follow-ups
(`add-component-render-smoke-pattern`, `scanner-disambiguation-playwright-smoke`).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-13 01:20:21 -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