Commit graph

15 commits

Author SHA1 Message Date
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
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
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
varutasu
e78f78e3f6
ci: run migrations against Postgres service container in CI (#64)
* ci: run migrations against Postgres service container in CI

Add a migrate job that applies node-pg-migrate against an ephemeral
Postgres 16 service container so broken migrations fail at PR time.

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

* fix(migrations): retimestamp scan tables after initial-schema.

Fresh CI/postgres runs failed because 1748365200000 sorted before
initial-schema. Renamed to 1779853647566 with IF NOT EXISTS guards intact.

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

* fix(migrations): ensure is_system_collection exists before description backfill.

Fresh CI Postgres runs initial-schema without this column (added historically
via scripts); ADD COLUMN IF NOT EXISTS makes the data migration safe on new
and existing envs.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 11:01:18 -05:00
varutasu
00193651aa
Backfill system collection descriptions to current vocabulary. (#60)
Existing is_system_collection rows may carry stale copy; align them with VOCAB.SYSTEM_COLLECTION_SEED_DESCRIPTION so UI matches new signups.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 00:42:23 -05:00
varutasu
673af83519
feat(scanner): persist scan captures to Blob (Brief 3) (#44)
Upload confirmed scan frames to Vercel Blob and store the URL on user_cards
when routing to owned cards, completing the redesign-scanner-flow convoy.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 13:57:26 -05:00
varutasu
d798e284c3
feat(scanner): AI Gateway vision + Layer-1 Tesseract/pg_trgm OCR (#38)
Route Layer-2 identification through Vercel AI Gateway (AI_GATEWAY_API_KEY,
default google/gemini-2.5-flash-lite). Add Layer-1 browser Tesseract name-strip
OCR with pg_trgm fuzzy catalog match via /api/cards/identify-by-text before
escalating to vision.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 12:59:59 -05:00
varutasu
e81dd49752
feat(scanner): server-side scan pipeline (convoy #2) (#35)
* feat(scanner): move card identification server-side (convoy #2)

Replace browser Gemini/OCR with POST /api/scan/identify, add card_submissions
review queue, remove user-writable cards INSERT, and surface disambiguation
when catalog matching is ambiguous.

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

* ci: allowlist server-only lib/scan-gemini.js in LLM key gate

The scan pipeline helper lives under lib/ but is imported exclusively
from pages/api/scan/identify — exclude it from the client-side URL scan.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 08:47:05 -05:00
varutasu
de9f3348f6
feat(infra): adopt node-pg-migrate + backfill initial schema migration (#32)
Closes P1 #11 of .convoys/ship-readiness.md (launch sequence step 7) —
"No migration tool — scripts/add-*.js graveyard". Schema changes
post-this-convoy ship as node-pg-migrate migrations under migrations/
at the repo root; the legacy 27 scripts/add-*.js / scripts/fix-*.js /
scripts/seed-*.js jobs remain append-only history per the no-go-zones
rule.

Decisions (full record in .convoys/migration-tool.md § Decisions):

D1 — Tool: node-pg-migrate@^8. Rejected drizzle-kit / prisma migrate /
kysely because each forces broader TypeScript surface than AGENTS.md
Gotcha #9 allows (TS is a devDep only). node-pg-migrate is
JavaScript-native, raw-SQL-friendly via pgm.sql(), and ESM-clean for
the post-bump-next-js "type": "module" repo. Brings pg@^8.21.0 as a
peer dep (dev-only; never loaded in the Next.js bundle).

D2 — Migrations directory: migrations/ at the repo root. Separates
the tool-wrapped artifacts from the historical scripts/migrations/
placeholder folder (which housed the lone pre-tool
2026-05-24-rename-admin-email.js migration and remains preserved for
the audit trail). Matches node-pg-migrate's default flag.

D3 — Tracking table: default pgmigrations (no name collision with
the existing 7-table bootstrap; zero CLI noise).

D4 — Backfill strategy: hand-translate scripts/setup-neon-db.js's
DDL into the initial migration verbatim. Each await sql`...` block
becomes one pgm.sql(`...`) call. Each CREATE uses IF NOT EXISTS, so
the migration is idempotent against fresh AND pre-existing envs —
re-running setup-db on an env that already has the schema is a no-op
DDL-wise (only records the pgmigrations row). Documented assumption:
prod has drifted via the 27 historical add-*.js scripts; reconciling
those into the migration history is the queued
reconcile-historical-add-scripts follow-up convoy.

D5 — Bootstrap reconciliation: split. setup-neon-db.js now (1)
validates ADMIN_INITIAL_PASSWORD + POSTGRES_URL, (2) spawns
`npm run migrate up` via child_process with stdio inherited, (3)
seeds the admin row with ON CONFLICT (email) DO NOTHING. The seven
DDL blocks are deleted from setup-neon-db.js; success/error message
copy is updated to mention the migration step explicitly.

D6 — CI integration: defer. Wiring a CI job that runs migrate up
against a test DB needs either a dedicated Neon branch + secret OR a
Postgres service container; both are real work. Surface as
wire-migrate-into-ci follow-up. Risk acknowledged in
.convoys/migration-tool.md § R3.

D7 — Down-migration on the initial backfill: hard stub. Rolling back
the initial schema would drop every user / card / collection / deck
row in the DB. The stub throws with a long-form error pointing at
the recommended alternative (branch the Neon database + forward-apply).
Future migrations that touch one of the seven bootstrap tables write
their own dated migration with a real down().

Verification (pre-PR):
- npm run lint → 128 problems (baseline preserved, zero regression;
  migration file is lint-clean, no new ignore patterns)
- npm run test:run → 21/21 pass
- node --check on migrations/1779853647564_initial-schema.js + on
  scripts/setup-neon-db.js → exit 0
- Module load + down() throw verified via dynamic import
- npm run migrate -- --help reaches the node-pg-migrate CLI through
  the wrapper

Live verification against a Neon branch is deferred (no throwaway
branch available); the operator's optional post-merge sequence is
documented in .convoys/migration-tool.md § Operator runbook.

See .convoys/migration-tool.md § Follow-ups for the queued
wire-migrate-into-ci / reconcile-historical-add-scripts /
retire-graveyard-scripts-after-audit / audit-node-pg-migrate-transitive-deps
/ add-migration-template follow-up convoys.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 23:01:58 -05:00