Commit graph

260 commits

Author SHA1 Message Date
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
varutasu
c51ec6a04c
Normalize collector numbers in catalog match and harden scanner adds. (#53)
Share card-number normalization across reconcile and identify paths, retry set/name matches when OCR uses leading-zero collector numbers, and extend in-flight locks to all scanner destination actions with disabled Mark Owned feedback.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 14:45:55 -05:00
varutasu
174a370fc3
Switch Pokémon catalog import to pokemon-tcg-data on GitHub. (#52)
Replace pokemontcg.io API discovery and import with raw JSON from PokemonTCG/pokemon-tcg-data; format collector numbers as number/printedTotal and drop the API key dependency for catalog sync.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 11:56:01 -05:00
varutasu
cf5c0558f1
Auto-link pending scan submissions after catalog sync imports. (#51)
When a set lands via runCatalogSync, match pending card_submissions by set/name/number to catalog rows and approve them with promoted_card_id instead of leaving them in the admin queue.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 09:47:59 -05:00
varutasu
8f3fbe70a2
Add admin panel button to trigger catalog sync. (#50)
Expose POST /api/admin/sync-catalog for authenticated admins (import rate limit, 300s timeout) and wire a Run catalog sync control on /admin/card-import.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 09:18:01 -05:00
varutasu
bec0a7abbd
Prioritize newest missing sets in catalog sync queue. (#49)
The cron was importing oldest MTG sets first and never reaching recent Pokémon releases like Perfect Order; merge MTG and Pokémon by release date descending instead.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 21:49:58 -05:00
varutasu
0a47362103
feat(catalog): weekly Vercel Cron sync for MTG and Pokémon sets (#48)
Extract shared import logic into lib/card-import, discover missing sets via
Scryfall/Pokémon TCG APIs, and expose GET /api/cron/sync-catalog protected
by CRON_SECRET (max 3 sets/run, paced imports).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 14:59:59 -05:00
varutasu
49d1e62fa7
test(scanner): cover redesign API and component surfaces (#47)
Add vitest specs for user-cards quantity validation, scan upload-image
auth/rate-limit/MIME gates, ScannerDestinationPicker, and ScannedCardItem.
Suite grows 21 → 37 tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 14:28:04 -05:00
varutasu
30b21b42c5
fix(api): validate quantity on POST /api/user-cards (#46)
Match decks handler parseInt/NaN guard so non-numeric quantities cannot
corrupt user_cards row counts from scanner add paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 14:25:20 -05:00
varutasu
66717c4198
fix(scanner): close redesign a11y audit findings (#45)
Add focus traps for modals, accessible names for icon/select controls,
ownership badge role=status, list semantics for the scan queue, and
aria-live updates for the card count.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 14:22:18 -05:00
Randall Stillwell
ff73743a9f docs(convoy): close redesign-scanner-flow post-PR audit
Record audit-redesign-scanner-flow-44 rollup and queue follow-up convoys
from reviewer, design-system, and a11y fan-out on PRs #42–#44.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 14:09:50 -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
24c9da4095
feat(scanner): condition, foil, quantity, and ownership badge (Brief 2) (#43)
Extract ScannedCardItem with per-card metadata controls and ownership
lookup via GET /api/cards/[id]/ownership. Propagate condition, foil,
and quantity through owned/collection/deck POST paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 13:54:33 -05:00
varutasu
47a1abbe4d
feat(scanner): stack-destination picker and auto-route (Brief 1) (#42)
Add ScannerDestinationPicker with game filter and owned/collection/deck
targets. Persist session destination in localStorage and auto-add each
identified scan to the active destination.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 13:50:54 -05:00
varutasu
55af7e3c90
fix(scanner): idempotent Mark-Owned and reliable bulk actions (#41)
Add per-row in-flight locks so double-tap cannot duplicate owned POSTs.
Pass bulk action/target directly instead of setTimeout state races.
Log collection card adds via logCollectionActivity and fix rows.length
checks in the collection cards POST handler.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 13:44:47 -05:00
varutasu
a251dacbd3
fix(scanner): catalog gap review path + not-listed disambiguation (#40)
When vision reads a set+number missing from the catalog, route to
card_submissions rather than sibling disambiguation. Adds a not-listed
modal action, background vision refine, foil-friendly prompt, and
submit-for-review API. Queues catalog-sync-vercel-cron convoy for later.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 13:42:51 -05:00
varutasu
8dc6dd6e26
fix(scanner): Layer-1 SQL, printing picker, and rate-limit storm (#39)
Fix identify-by-text 500 (Neon could not infer null game param type).
When a card name has multiple catalog printings, show disambiguation
instead of auto-picking the first match. Throttle concurrent vision
calls and suppress repeated 429/error toasts during detection.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 13:10:46 -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
be5dd8027f
fix(scanner): gemini-2.5-flash model + actionable scan error messages (#37)
Use the same vision model as the deleted browser client, surface Gemini
quota/denial/migration failures as 502/503 with readable text, and stop
scan_attempts telemetry from blocking identification.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 12:42:37 -05:00
varutasu
ecb3ee12fc
fix(auth): wait for verify before scanner redirect; fix admin hooks (#36)
Scanner was sending logged-in users to /login while useAuth was still
loading. Admin card-editor/card-import crashed on login because hooks
ran after a mounted early return (Rules of Hooks violation).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 12:36:58 -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
8c58990fd9
fix(security): stop leaking Gemini API key to browsers (#34)
Delete the public /api/config/gemini endpoint and remove client auto-load
paths so GEMINI_AI_API_KEY stays server-side only. Add a scan rate-limit
class for the upcoming server-side identify route and a CI gate that blocks
reintroducing config key leaks or new browser LLM URLs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 08:41:48 -05:00
varutasu
7efb6efe69
docs: post-convoy cleanup for 7-convoy 2026-05-26 wave (#33)
Updates ship-readiness.md, AGENTS.md, and 7 convoy files to reflect
the as-shipped state of the 2026-05-26 7-convoy multitask wave:

- PR #26 tighten-visual-diff-path-filter (P3)
- PR #27 purge-weak-creds-from-helpers (P2, closes the umbrella)
- PR #28 cleanup-mobile-nav-dead-props (P3)
- PR #29 lint-against-cjs-in-esm-scripts (P3, surfaced by PR #25)
- PR #30 single-sql-client (P1 #8 RESOLVED)
- PR #31 single-auth-provider (P1 #9 RESOLVED)
- PR #32 migration-tool (P1 #11 RESOLVED)

Milestone: 5 of 6 P1 quality items RESOLVED. Only fix-lint-baseline
(P1 #11.5) remains in the P1 lane.

Newly queued follow-ups:
- purge-quick-login-from-loginpage (surfaced by PR #27)
- purge-neondatabase-serverless-fully (surfaced by PR #30,
  unblocked by PR #32's migration tool adoption)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 23:15:21 -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
varutasu
0668b0c7d0
refactor(auth): collapse lib/auth-context.js + lib/admin-auth.js onto lib/use-auth.js (#31)
`lib/use-auth.js` is now the sole client-side auth surface (P1 §9 of
`.convoys/ship-readiness.md`). The legacy `lib/auth-context.js`
(`AuthProvider` + `useAuth`) and `lib/admin-auth.js` (`AdminProvider` +
`useAdmin` + `useIsAdmin`) are deleted; every importer is migrated to
the canonical hook. Pre-convoy a worst-case page mount issued THREE
identical `GET /api/auth/verify` requests (one per provider/hook); the
post-convoy floor is one verify per page mount (3 → 1 on
`pages/card/[id].js`, 2 → 1 elsewhere).

Importer inventory swept (7 source files):
- `pages/_app.js` — removed `<AuthProvider>` wrapper; `<ThemeProvider>`
  is now the only top-level provider. `lib/use-auth.js` is hook-only,
  no replacement provider needed.
- `pages/index.js`, `pages/scanner.js`, `pages/decks.js`,
  `pages/deck/[id].js`, `pages/deck-builder.js` — `import { useAuth }`
  path swap from `../lib/auth-context` to `../lib/use-auth`. All five
  pages destructured only `{ user }` or `{ user, loading }`; verified
  no consumer reads `login` / `register` from useAuth (those flows are
  in `pages/login.js` / `pages/signup.js` which call the API directly),
  so no shape-parity gap on `lib/use-auth.js`.
- `pages/card/[id].js` — replaced `useIsAdmin()` (the only consumer of
  `lib/admin-auth.js` anywhere in the tree) with synchronous
  `user?.role === 'admin'` derived from the existing `useAuth()` call.
  Render condition at line 524 stays byte-identical.

Decisions documented in `.convoys/single-auth-provider.md`:
- D1: no extension to `lib/use-auth.js` (zero call sites for `login` /
  `register` from useAuth — those flows are direct fetches in
  `login.js` / `signup.js`).
- D2: `useIsAdmin()` collapses onto `useAuth()`; no separate hook.
- D3: provider tree `<ThemeProvider><AuthProvider>{children}</AuthProvider></ThemeProvider>`
  → `<ThemeProvider>{children}</ThemeProvider>`.
- D4: 3 → 1 verify roundtrip on `card/[id].js`; 2 → 1 on every other
  page-load.
- D5: zero test files modified; the 21-test vitest suite is server-
  side or prop-driven (`Layout.test.js` passes `user` as a prop, never
  imports the legacy hooks).

Doc / config updates so the deletion lands cleanly:
- `.github/CODEOWNERS` — drop the two CODEOWNERS lines for the deleted
  files.
- `AGENTS.md` § 2 architecture row + § 3 "Auth (client)" bullet —
  rewritten for the post-convoy single-surface state.
- `.cursor/rules/auth-and-permissions.mdc` — § "Legacy" reframed to
  "deleted by this convoy"; § "Authentication state on the client"
  updated to the post-convoy `useAuth()` shape and the direct-fetch
  login flow used by `login.js` / `signup.js`.
- `.cursor/rules/no-go-zones.mdc` — auth-refactors bullet drops the
  deleted files from the canonical list.
- `.cursor/skills/add-page/SKILL.md` — checklist + anti-pattern row
  refer to the deletion.

Verification:
- `rg "lib/auth-context|lib/admin-auth" --type js` → 0 hits in source.
- `npm run lint` → 128 → 125 problems (3 fewer errors from the deleted
  unused-import lines; no regression).
- `npm run test:run` → 21/21 pass (including the 5 Layout regression
  locks from `fix-layout-default-user`, which are prop-driven and
  unaffected).
- `npm run build` → all 26 pages compile end-to-end; no SSR / static-
  generation breakage that would have surfaced if a page tried to use
  the legacy context hook unwrapped.
- Manual smoke deferred to operator post-merge per convoy doc.

Risks (full discussion in convoy file):
- R1 shape parity gap — verified zero consumers of legacy-only
  surface; mitigated.
- R2 SSR mismatch from removing `<AuthProvider>` — `useEffect`-
  guarded `localStorage` read; identical SSR shape pre/post; build
  passes.
- R3 missed importer — post-delete grep + build pass would surface
  any miss.
- R5 stale `useAuth` cache across components — pre-existing
  pattern, called out as follow-up rather than addressed here.

Out of scope: any change to `lib/permission-middleware.js` (server-
side; resolved P0 #1), `lib/auth-secret.js` (resolved P0 #2),
`pages/api/**` route handlers, login / register API contracts, or
the seeded admin account flow.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 22:58:08 -05:00
varutasu
c403ea49e8
refactor(db): collapse @neondatabase/serverless onto @vercel/postgres + delete lib/database.js (#30)
Convoy: single-sql-client (P1 quality, launch sequence step 8)
Addresses: AGENTS.md Gotcha #1, .convoys/ship-readiness.md P1 #8

## Decisions

- D1: Caller inventory = 2 files (1 source + 1 test), not "~3 based on graph".
  Only pages/api/auth-utils.js imports `db`; test/api/auth-utils.test.js mocks
  it purely to satisfy the import graph (the 5 tests exercise
  generateToken/verifyToken, not isAdmin/getUserById).
- D2: Migrate both call sites (isAdmin, getUserById) to @vercel/postgres
  tagged-template SQL. Queries are SELECT-only, single-table,
  single-numeric-parameter — byte-equivalent translation; same result shape
  ({rows, rowCount}); no transaction or pool semantics differ.
- D3: KEEP @neondatabase/serverless as a dep. 11 scripts/* files still use
  `neon()` directly (setup-neon-db.js, migrations/, reset-db.js, 8 historical
  add-*/fix-*/seed-* jobs). They are out of scope per the no-go-zones rule
  and the convoy spec; purging the dep entirely would be its own convoy
  (queued as `purge-neondatabase-serverless-fully`, blocked on migration-tool).
- D4: sql.unsafe audit — NOT a real injection vector with current callers
  (userId comes from a verified JWT, is a numeric SERIAL id). Security
  finding: NO. Pure refactor + foot-gun removal that prevents the FUTURE
  caller that would have been the incident.
- D5: Test mock cleanup — drop the now-unneeded `vi.mock('../../lib/database.js')`
  call + unused `vi` import. Test count + assertions unchanged (5/5).

## Per-file changes

- pages/api/auth-utils.js: swap `import { db } from '../../lib/database.js'`
  for `import { sql } from '@vercel/postgres'`; rewrite isAdmin's
  `db.query(SELECT … WHERE id = $1, [userId])` and getUserById's same shape
  to `sql\`SELECT … WHERE id = ${userId}\``. Same try/catch, same
  result.rows[0] access, same error returns.
- test/api/auth-utils.test.js: drop vi.mock for lib/database.js + the unused
  `vi` import. 5/5 tests still pass.
- lib/database.js: DELETED (47 lines removed; manual-interpolation + sql.unsafe
  wrapper is gone).
- .convoys/single-sql-client.md: NEW (the convoy file documenting all
  decisions + caller inventory + verification + risks + follow-ups).

## Verification

- npm run lint → 128 problems (baseline preserved, no regression)
- npm run test:run → 21/21 pass (vitest)
- Grep "lib/database" --type js -l → 0 hits anywhere
- Grep "@neondatabase/serverless" --type js -l → still matches the 11
  scripts/* sites (expected; out of scope per D3)
- node --check pages/api/auth-utils.js → exit 0

## Scope note

This convoy collapses the lib/database.js abstraction onto the canonical
@vercel/postgres surface for pages/api/**. It does NOT eliminate
@neondatabase/serverless from the dependency tree — that would require
migrating the scripts/* helpers, which is out of scope here (no-go-zones
rule + convoy spec). Queued as a follow-up.

## Live smoke

Deferred. The two migrated functions (isAdmin, getUserById) are only
reachable via pages/api/admin/index.js which requires an admin Bearer
token and a populated users table in prod Neon. Byte-equivalent SQL +
identical result shape gives high confidence; rollback is a single-commit
revert if a post-merge admin action 500s.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 22:54:01 -05:00
varutasu
13d62108ce
chore(lint): forbid require() in scripts/** under "type": "module" (#29)
Add an ESLint no-restricted-syntax rule scoped to scripts/**/*.js
that flags any CallExpression with callee name `require`. The
recurring bug pattern: helper scripts under scripts/ that use CJS
require() throw `ReferenceError: require is not defined` on Node
22.x because package.json has had "type": "module" since
bump-next-js. The bug has bitten twice in two convoys — once in
drop-public-setup Brief 2 (setup-neon-db.js, commit b63b509) and
again in fix-reset-db-script (reset-db.js, PR #25 squash 3ab9bf8).
Both were caught at first run, not at lint time. This rule would
have caught both at PR time.

Rule shape: a second flat-config block at the end of
eslint.config.mjs (NOT in the root rules block) targeting only
scripts/**/*.js. The error message points at
.convoys/fix-reset-db-script.md so the next agent who trips it
gets a 1-click path to the exemplar fix (ESM top-level imports
for dotenv, neon, bcrypt) instead of having to re-derive it.
scripts/migrations/** is already in globalIgnores from pick-a-name
Brief 2 and stays excluded.

Blast-radius rationale (scripts/** only, not all .js at repo root):
matches the actual observed bug surface. pages/api/** is already
correctly ESM-imported throughout (verified across add-rate-limiting,
cors-tighten, and the add-route skill). The config files
(postcss.config.js, tailwind.config.js, next.config.js) intentionally
use CJS-style exports that the next-config base rules already handle
correctly. A repo-wide ban would produce zero true positives outside
scripts/** today and would require explicit allowlist for every
config file — strictly more code, more maintenance, zero benefit.

Convoy file: .convoys/lint-against-cjs-in-esm-scripts.md (P3 polish,
parent-owned, no architect — preventative one-line rule following
two proven bug recurrences).

Verification:
- node --check eslint.config.mjs: exit 0
- npm run lint: 128 problems (81 errors, 47 warnings) — baseline
  preserved verbatim, zero new false positives in current tree
- Negative test: prepended `const x = require('fs');` to
  scripts/reset-db.js, ran npm run lint, observed exit 1 with
  129 problems and the rule firing at line 20:11 with the
  documented message, then reverted to 128 problems clean
- npm run test:run: 21/21 pass (no test surface touched)
- Grep: 0 require( occurrences in scripts/**/*.js (current tree
  is clean; rule starts with zero positives to silence on day 1)

Surfaces no new follow-up — this convoy IS the follow-up surfaced
by fix-reset-db-script.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 22:53:31 -05:00
varutasu
171f5afc6f
chore(components): remove dead user prop from MobileNavigation (#28)
components/MobileNavigation.js has accepted a `user` prop ever since
the mobile bottom-bar was extracted from Layout, but it has never read
any field of `user`. The bottom-bar items (Cards, Decks, Dashboard,
Community, More) are statically configured — none of them branch on
auth state, role, user id, or any other per-user attribute. The prop
is dead.

This was originally surfaced as R8 in the fix-layout-default-user
convoy (commit ca302a8) and deliberately deferred there to keep that
convoy focused on the Layout default-user fix. The follow-up was
queued as cleanup-mobile-nav-dead-props in .convoys/ship-readiness.md
§ Queued convoys. Pre-edit audit confirms the queue entry's premise:
`rg '\\buser\\b' components/MobileNavigation.js` returns 1 hit (the
destructure on line 5) before the change and 0 hits after. The
only active call site is components/Layout.js line 598; the
components/Layout.js.backup snapshot also calls it but is a
no-go-zone (per .cursor/rules/no-go-zones.mdc § "Append-only /
historical") and stays untouched — when that backup is eventually
deleted in a separate convoy, its stale call disappears with it.

Verification: npm run lint exit 1 with 128 problems (baseline
preserved, no regression introduced); npm run test:run 21/21 pass
(test/components/Layout.test.js still asserts the logged-out branch
contract from PR #15 — the dead-prop removal is invisible to that
suite since it does not inspect MobileNavigation's prop shape).
Convoy file .convoys/cleanup-mobile-nav-dead-props.md captures the
audit, fix, risks (R1: a future per-user bottom-bar feature would
need to re-add the prop — accepted; carrying dead state to hedge
hypothetical features is worse than paying the one-line re-add cost
when the feature actually lands), and acceptance criteria.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 22:53:28 -05:00
varutasu
5f2b234cc8
fix(scripts): require TEST_USERS_PASSWORD + purge weak literals from test-user helpers (#27)
`scripts/create-test-users.js` hardcoded `bcrypt.hash('alice123', 12)`
+ `bcrypt.hash('bob123', 12)` and echoed those literals back to stdout
both per-user and in a final summary block. `TESTING_GUIDE.md`'s Test
Accounts table documented the same `admin123` / `alice123` / `bob123`
trio. These were the last two weak-credential surfaces left in the
helper-script + manual-QA-doc tree after `drop-public-setup` (commits
`ff80753` + `b63b509`) and `fix-reset-db-script` (squash `3ab9bf8`,
PR #25) closed the `setup-neon-db.js` and `reset-db.js` halves of the
umbrella `purge-weak-creds-from-helpers` queued follow-up.

The fix mirrors the post-`drop-public-setup` `setup-neon-db.js`
pattern and the post-PR-#25 `reset-db.js` pattern verbatim, with one
deliberate simplification: a single `TEST_USERS_PASSWORD` env var
covers both alice + bob rather than per-user env vars (risk R2 in the
convoy file argues this — these are fixture users for the
collaboration demo flow, not independent identities, and per-user
sprawl would double the env-var contract for zero security benefit).
`createTestUsers()` now reads `process.env.TEST_USERS_PASSWORD` at the
top of the function body and exits with code 1 BEFORE opening any DB
connection if the var is unset or whitespace-only, with the same
helpful-error wording template the other two scripts use (names the
var, points at `.env.local`, suggests `openssl rand -base64 24`,
references README's "First-time admin setup" section). All four
password-echo `console.log` lines are deleted; the new summary
documents *where* the password comes from without ever printing it.
`TESTING_GUIDE.md`'s Test Accounts table is rewritten to show password
source per user instead of the literal value; the two inline
`Password: alice123` / `Password: bob123` workflow snippets are
replaced with placeholder text. Unlike the previous two convoys, no
CJS→ESM conversion was needed — `create-test-users.js` was already
top-level ESM.

Verification (all static — script is destructive and not live-tested):
`node --check scripts/create-test-users.js` exit 0; `npm run lint` 128
problems (baseline preserved, no regression); `npm run test:run` 21/21
pass; grep `scripts/ TESTING_GUIDE.md` for
`admin123|password123|test123|alice123|bob123` → 0 hits;
`TEST_USERS_PASSWORD` referenced 10 times total (5 script + 5 doc).
Operator caveat: anyone running `node scripts/create-test-users.js`
post-merge must add `TEST_USERS_PASSWORD=<value>` to their
`.env.local` first; existing alice + bob rows in already-seeded
environments are NOT rotated by re-running this script
(`ON CONFLICT (email) DO NOTHING` preserves the old hashes). Same
caveat that applies to the `drop-public-setup` admin row.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 22:53:24 -05:00
varutasu
ba954629ed
ci(workflows): exclude pages/api/** from visual-diff path filter (#26)
The `Screenshot diff` workflow's `paths:` filter included `pages/**`
which matches `pages/api/**` too, so API-only PRs triggered the
visual-diff workflow even though they can't possibly move a single
rendered pixel. PR #19 (cors-tighten) and PR #20 (add-rate-limiting)
both empirically hit this — each was an API-only sweep, and each
burned ~55s of CI runtime on a `Screenshot diff` job that
`continue-on-error: true` then swallowed. Documented as a queued
follow-up in `.convoys/ship-readiness.md` § Queued convoys, ratified
for fix in this convoy.

The fix is a single negated-glob entry inserted immediately after
`pages/**` in the `paths:` list. GitHub Actions evaluates `paths:`
with minimatch and supports `!`-prefixed exclusions per the
published path-filter cheatsheet, but the order matters: a `!pattern`
only takes effect if it appears AFTER an include that already
matched the path. Keeping `!pages/api/**` second in the list (right
after `pages/**`, before all the other includes) is the canonical
shape. All five existing entries are preserved verbatim; only the
one exclusion entry plus an inline comment explaining the ordering
rule and the empirical motivation is added.

`preview-smoke.yml` is intentionally untouched — verified its `on:`
block has no `paths:` filter at all (it triggers on every PR
targeting main, with skip-via-PR-body-directive in the gate job),
so there's no false-positive shape to fix there. Smoke SHOULD run on
every PR including API-only ones because changes to `pages/api/**`
can break the home redirect + sign-in + `/api/health` endpoints the
smoke spec exercises.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 22:51:22 -05:00
Randall Stillwell
5f1fa60cba docs: post-convoy cleanup for fix-reset-db-script
Fill in fix-reset-db-script.md § As-shipped with the merged-state record
(PR #25, squash 3ab9bf8, +161/-18) — three bugs atomically resolved by
mirroring setup-neon-db.js post-drop-public-setup shape; CI green
including 5th-consecutive smoke-spec cross-validation; lint-against-
cjs-in-esm-scripts surfaced as new P3 queue entry.

Update ship-readiness.md Queued convoys:
- convert-reset-db-to-esm → RESOLVED (audit-trail kept, not removed)
- purge-weak-creds-from-helpers → scope reduced (reset-db.js half done;
  create-test-users.js + TESTING_GUIDE.md remain)
- lint-against-cjs-in-esm-scripts → NEW (P3 polish; would have caught
  both DPS-B2 and PR #25 at lint time)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 22:10:13 -05:00
varutasu
3ab9bf840c
fix(scripts): convert reset-db.js to ESM + require ADMIN_INITIAL_PASSWORD (#25)
Fold of two queued follow-ups from pick-a-name architect audit
(convert-reset-db-to-esm + purge-weak-creds-from-helpers). Three bugs
in one file; all three fixed atomically by mirroring the proven post-
drop-public-setup setup-neon-db.js shape (commit b63b509).

Bugs fixed:
1. CJS-in-ESM (lines 10, 12, 142): require('dotenv'), require('@neon...'),
   inline require('bcryptjs'). package.json has "type": "module" since
   bump-next-js, so npm run reset-db threw ReferenceError on Node 22.x.
   Same bug pattern that hit setup-neon-db.js pre-drop-public-setup B2.
2. Hardcoded weak admin password (line 143: bcrypt.hash('admin123', 12)).
   Same anti-pattern drop-public-setup B1 removed from setup-neon-db.js.
3. Password echoed to stdout (line 156: console.log('Admin Password:
   admin123')). Security anti-pattern; setup-neon-db.js post-DPS does
   NOT echo passwords.

Fix shape (verbatim mirror of setup-neon-db.js):
- ESM top-level imports (dotenv, neon, bcrypt)
- Fail-loud ADMIN_INITIAL_PASSWORD env-var check at function top with
  helpful error message pointing to README "First-time admin setup"
- bcrypt.hash(adminPassword, 12) instead of literal
- ON CONFLICT (email) DO NOTHING on INSERT (defensive against
  double-run, matches setup-neon-db.js line 149)
- No password echo in success block; admin email logged for confirmation
- Updated docstring to flag DESTRUCTIVE + reference required env

Convoy file: .convoys/fix-reset-db-script.md (P2 hygiene, parent-owned,
no architect — this is a proven-pattern fold with no new decisions
to ratify).

Verification:
- node --check scripts/reset-db.js: exit 0
- npm run lint: 128 problems (baseline preserved, no regression)
- npm run test:run: 21/21 pass
- Grep: 0 require( | 0 admin123 | 0 'Admin Password' in scripts/reset-db.js
- Grep: 3 ADMIN_INITIAL_PASSWORD references (docstring, const, error msg)

NOT live-tested (script is destructive — drops all tables). Operator
can optionally run npm run reset-db against a non-prod Neon branch
post-merge to verify end-to-end.

Surfaces follow-up: lint-against-cjs-in-esm-scripts (P3 polish — add
ESLint rule to prevent any future require() in scripts/** under
"type": "module"). Surfaced for future convoy queue.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 22:08:45 -05:00
varutasu
1de0e03522
fix(migration): rename-admin-email crashes — neon() returns array, not { rows }
Hotfix to scripts/migrations/2026-05-24-rename-admin-email.js (shipped 2026-05-24 in pick-a-name PR #21). Script crashed on first invocation with 'TypeError: Cannot read properties of undefined (reading length)' at line 44. Root cause: architect designed against @vercel/postgres return shape { rows, rowCount } but the script uses @neondatabase/serverless's neon() tagged template which returns the rows array directly. AGENTS.md Gotcha #1 (two SQL clients in parallel) is exactly this kind of cross-contamination. Fix: drop the { rows: x } destructuring in all 3 sites + add a 4-line why comment block above the first site so the next migration author doesn't repeat. Verified hand-run against prod Neon DB: migrated 3 users (admin id=1, alice id=5, bob id=6) from @tcgvault.com to @deckhearth.com; idempotent re-run prints 'Nothing to migrate.' No data risk on the original crash — script exited at line 44 before reaching the UPDATE at line 54. PR #21 operator action item now complete in prod. Surfaces a P3 follow-up: add-neon-return-shape-rule (or fold into single-sql-client). All CI green: lint 128 baseline, vitest 21/21, Playwright smoke 3/3 in 1m2s, forbidden-cors-headers pass, forbidden-endpoints pass. PR #24, commit 98406fa.
2026-05-25 11:30:09 -05:00
varutasu
7e06ab559a
docs: post-convoy cleanup for pick-a-name — first post-P0 P1 convoy
Post-merge doc cleanup for PR #21. Resolves the AGENTS.md line-5 'Pick one before launch' branding question. Updates AGENTS.md top branding note + Gotcha #4 + Gotcha #12, adds full as-shipped to .convoys/pick-a-name.md, adds 4 queued-convoy entries to ship-readiness.md, adds pick-a-name attribution to .cursor/rules/api-routes.mdc. Pure docs (+357/-15 across 4 files).
2026-05-25 04:10:12 -05:00
varutasu
9abbab6c21
feat(brand): unify on Deck Hearth across in-repo strings + infra (P1 brand decision)
Resolves the launch-blocking 'TCG Vault vs Deck Hearth' inconsistency called out in AGENTS.md line 5 since project setup. Operator gate-0 decision: Deck Hearth wins. Two briefs applied serially. B1 (mechanical): 7-file display + comment sweep. B2 (infrastructure): Redis prefix rename in lib/rate-limit.js (5 prefixes, accept one-time counter reset), package.json + lockfile regen (STOP-on-churn confirmed only name lines changed), admin/alice/bob email rename in seed scripts + login pre-fill + NEW idempotent migration script scripts/migrations/2026-05-24-rename-admin-email.js. Risk 4 PRESERVE applied: test/lib/permission-middleware.test.js retains admin@tcgvault.com literal with 7-line architect-authored why comment (documents pre-fix-auth-bypass bug shape; preserves historical truth per project's gotcha-documentation convention). All 5 D-decisions ratified at gate-1 (Deck Hearth / deck-hearth / deckhearth / admin@deckhearth.com / full deckhearth Redis prefix). Local: lint 128 baseline (B1 + B2), vitest 21/21 (B1 + B2). CI all green: Playwright smoke 3/3 against rebranded preview in 1m4s, forbidden-cors-headers pass, forbidden-endpoints pass, Screenshot diff pass, Vercel deployment complete. Cross-validation lineage: 4th convoy where the same 3-test smoke spec defends auth surface through sweeping change (after PR #15 Layout default-user, PR #19 CORS, PR #20 rate-limit, now this PR #21 brand rename). OPERATOR POST-MERGE ACTION REQUIRED: run 'node scripts/migrations/2026-05-24-rename-admin-email.js' against prod Neon DB before next admin login (ordering: migration FIRST, then any subsequent setup-db invocation). Migration is ESM, idempotent, UNIQUE-collision-safe. PR #21 architect-commit 50ce9ab, B1 ac8c998, B2 1c18d21.
2026-05-25 02:28:29 -05:00
Randall Stillwell
7832e03dec docs: post-convoy cleanup for add-rate-limiting — MILESTONE, last P0 closed
Reflects the merged add-rate-limiting convoy (PR #20, squash commit
708ef45) in repo documentation. **This is the milestone cleanup** —
add-rate-limiting closed P0 #6 (No rate limiting anywhere), the LAST
open P0 ship-blocker. `.convoys/ship-readiness.md`'s § Status summary
flips from "7 of 8 RESOLVED; 1 remains" to **"8 of 8 RESOLVED.
Launch-readiness P0 checklist is empty."** One brief in the convoy:
Brief 1 shipped as planned with no scope expansions and no implementer
deviations from the verbatim spec; all six architect decisions ratified
verbatim at gate 1 (D1 operator-ratified Option A; D2-D6
architect-self-ratified).

.convoys/add-rate-limiting.md:
  - frontmatter status: in-progress -> shipped (added shipped: 2026-05-24)
  - new ## As-shipped section. Opens with the milestone language
    pointing back at ship-readiness.md's flipped § Status summary.
    Decisions section captures all 6 ratifications (D1 operator-
    ratified Option A — Critical: WHY the atomic admin UI fix in
    pages/admin/card-import.js was Decision 1's hidden coupling
    requirement, since API gating alone would have broken every
    "Import Cards" click; D2 hybrid named-limiter shape with
    Map<className, Ratelimit> cache; D3 per-class table including
    the two D3 tuning-evidence raises — search 30 -> 60/min because
    ShareModal.handleSearch has no debounce so a 17-char email = 16
    requests in <5s, and generate kept at 5/hour because DiceBear is
    free not paid AI; D4 two-extractor shape with defensive THROW
    on null/empty userId; D5 uniform 429 message; D6 no new vitest
    or playwright specs deferred to fill-vitest-handler-coverage).
    As-shipped surface broken into 4 layers (1 lib refactor + 6 route
    gates + 1 atomic admin UI fix + 1 rule extension) mirroring the
    cors-tighten cleanup's pattern-split shape. Empirical CI metrics
    from post-merge run 26382185019 (Playwright smoke 59s 3/3 in
    3.8s, forbidden-cors-headers pass, vitest 21/21, lint 128 baseline,
    Screenshot diff continue-on-error swallow per Decision 4).
    Cross-validation finding: smoke test 2 still passes against the
    post-rate-limit preview — that's three convoys in a row (PR #15
    Layout default-user, PR #19 CORS-tighten, PR #20 rate-limiting)
    where the same 3-test smoke spec defended the auth surface
    through sweeping changes. Operator-action-required: none. What
    did NOT change audit trail.

.convoys/ship-readiness.md:
  - § Status summary at the top flipped from 7/8 to 8/8 RESOLVED.
    Header text updated to "Launch-readiness P0 checklist is empty."
    P0 #6 row in the table flips from PARTIAL to RESOLVED with the
    two-convoy lineage (fix-auth-bypass Brief 4 + add-rate-limiting).
    Trailing paragraph rewritten as a milestone note: security gate
    closed; remaining launch work is P1 quality bar + P2/P3 polish.
  - P0 #6 entry flipped from PARTIAL to RESOLVED with the full
    add-rate-limiting as-shipped block (8 sub-bullets covering the
    lib refactor shape, the per-class table, the defensive THROW,
    the three import routes' auth-gating, the atomic admin UI fix
    and WHY, the rule extension, the 6 decisions, and the diff
    breakdown). Brief 4's 2026-05-23 partial is preserved as the
    prior as-shipped layer to maintain the audit trail.
  - Launch sequence step 4 marked RESOLVED 2026-05-24 with the
    squash commit + smoke metrics inline.
  - Queued convoys: removed the add-rate-limiting entry (it shipped).
    Added a new delete-dead-lorcana-import entry (P3 polish; the
    Lorcana import route was gated defensively in PR #20 despite
    zero current frontend callers — pages/admin/card-import.js's
    <select> only offers mtg + pokemon — so if Lorcana stays
    permanently out of the admin UI, this is the cleanup PR).
    Added three "flagged but kept out of scope" follow-ups per the
    convoy's § What did NOT change: harden-multipart-parser (P2;
    5MB body still consumed before the 429 path on avatar.js),
    god-function-split / refactor-cards-search-sql (P2; 240-line
    7-branch SQL in cards/search.js), and withAdmin(handler) wrapper
    extraction (P3 DX; the three import routes are call sites #3-5
    in the codebase but uniform inline shape was preserved for
    convoy atomicity). Updated tighten-visual-diff-path-filter to
    note PR #20 also tripped the same false-positive.

AGENTS.md:
  - Gotcha #12 extended end-to-end. Was the single-class auth-only
    lib + the env-var contract; is now the 5-class reality with a
    full per-class table (helper / limit-window / key / routes),
    the defensive THROW pattern in extractUserIdentifier, the
    gate-ordering rule for per-user limiters, and the
    auth → admin-role → rate-limit ordering for the three import
    routes. Prominent milestone line opens the new content:
    "add-rate-limiting convoy (squash 708ef45, PR #20, 2026-05-24)
    closed P0 #6 — all 8 P0s now RESOLVED." Original env-var
    contract paragraph (KV_REST_API_URL / KV_REST_API_TOKEN,
    fail-closed-in-prod / warn-and-noop-in-dev) is preserved
    verbatim above the new content.
  - § 6 Testing: intentionally untouched (no test surface changed;
    vitest 21/21 and smoke 3/3 still apply).
  - § 7 Deployment: intentionally untouched (no deployment-shape
    changed; same KV_REST_API_* env vars from Brief 4).

.cursor/rules/api-routes.mdc:
  - The implementer extended § Rate limiting in PR #20 with the
    per-class table + verbatim call shape + gate-ordering rules +
    identifier-extraction + uniform 429 + fail-closed env-var
    contract + fail-open Upstash-outage behavior. Doc-writer pass
    verified completeness; added a one-sentence convoy-attribution
    line at the top of § Rate limiting citing the two-convoy
    lineage (fix-auth-bypass Brief 4 for the auth class +
    add-rate-limiting for the other four classes and 7 newly-gated
    routes), mirroring the post-cors-tighten § CORS attribution
    shape. No other touch-ups needed.

No changes to: package.json, package-lock.json, lib/rate-limit.js,
pages/**, components/**, scripts/**, test/**, tests/**,
.github/workflows/**, README.md, TESTING_GUIDE.md, playwright.config.js,
eslint.config.mjs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 23:09:07 -05:00
varutasu
708ef45a96
feat(security): rate-limit search/upload/import + gate import routes (P0 #6 - closes last P0)
Closes P0 #6 from PARTIAL to RESOLVED. 8/8 P0s now closed. Extends lib/rate-limit.js from single-class to 5 named limiters (auth/search/upload/generate/import). Atomically gates the 3 import routes (auth + admin-role check + rate limit) and fixes pages/admin/card-import.js's missing Bearer header in the same commit (architect's critical discovery: API gating alone would have broken the admin UI). Per Decision 1 Option A. 10 files +185/-23. Local: lint 128 baseline, vitest 21/21. CI: Playwright smoke 3/3 in 3.8s, forbidden-cors-headers pass, all gates green. PR #20 architect-commit 60b842e, implementer-commit 51a3a97. Brief 4's login.js + register.js byte-identical.
2026-05-24 22:59:59 -05:00
Randall Stillwell
f8b2cd85bc docs: post-convoy cleanup for cors-tighten
Reflects the merged cors-tighten convoy (PR #19, squash commit da50d78)
in repo documentation. Closes P0 #5 (Wildcard CORS on API surface) from
PARTIAL -> RESOLVED, leaving only P0 #6 (full add-rate-limiting) open
of the original P0 ship-blocker set. One brief in the convoy: Brief 1
shipped as planned with no scope expansions and no implementer deviations
from the verbatim spec.

.convoys/cors-tighten.md:
  - frontmatter status: in-progress -> shipped (added shipped: 2026-05-24)
  - new ## As-shipped section: all 5 architect-self-ratifiable decisions
    ratified verbatim (D1 Option B / D2 delete OPTIONS / D3 moot / D4
    no new tests / D5 add CI lock); Pattern split (16 Pattern A + 8
    Pattern B) per architect's 10-file audit + implementer's per-file
    diff review; diff size (25 files, +29/-261); empirical CI metrics
    from post-merge run 26378806555 (forbidden-cors-headers 4s PASS,
    Playwright smoke 56s 3/3 in 3.3s, Screenshot diff continue-on-error
    0 with the documented Decision-4 missing-baseline failure beneath);
    cross-validation that Playwright smoke continues to pass post-CORS
    removal (the auth + public surfaces don't depend on the wildcard
    header); implementer subagent-retry footnote (HEAD already at
    a843736 when retry woke up - transient retry, work is canonical);
    operator-action-required-going-forward: none; What did NOT change
    audit trail.

.convoys/ship-readiness.md:
  - new ## Status summary at the top (right after the code-graph line):
    P0 set is now 7/8 RESOLVED; only #6 (rate-limiting) remains. Table
    lists each P0 with its resolving convoy + squash commit for a quick
    scan of remaining work.
  - P0 #5 marked RESOLVED 2026-05-24. Added the cors-tighten as-shipped
    block (24 files swept, new CI job, 16/8 Pattern split, 5 decisions
    ratified, diff stat, post-merge CI metrics, transient retry
    footnote, operator-action: none). Brief 4's 2026-05-23 partial
    is preserved as the prior as-shipped layer above the cors-tighten
    layer to maintain the audit trail.
  - Queued convoys: removed the cors-tighten entry (no longer queued).
    Added a new tighten-visual-diff-path-filter entry (P3 polish) -
    Screenshot diff workflow triggered on API-only PR #19 because its
    paths: filter is pages/** which matches pages/api/** too. ~55s of
    CI waste per API-only PR; one-line YAML tweak; verify GitHub
    Actions' negated-glob semantics before merging.

.cursor/rules/api-routes.mdc:
  - new ## CORS section near the existing ## Dev/test endpoints
    (removed) section. Documents the no-CORS-by-default convention,
    the brief-4 + cors-tighten lineage, the new forbidden-cors-headers
    CI gate, and three forward-conventions (no setHeader for CORS,
    no OPTIONS preflight handlers, design a proper middleware layer
    if a future cross-origin caller is needed - not wildcards in
    individual handlers).

AGENTS.md intentionally untouched. Gotcha #5 (the public
setup-database.js endpoint) is already RESOLVED by fix-auth-bypass
Brief 3 and unrelated to this convoy. The new convention belongs in
.cursor/rules/api-routes.mdc (where API conventions live) rather than
AGENTS.md; the convoy file + the new CI gate are sufficient
documentation for the audit trail. Per convoy spec, no new gotcha
entry needed.

No changes to: package.json, package-lock.json, pages/api/**, lib/**,
components/**, scripts/**, test/**, tests/**, .github/workflows/**,
README.md, TESTING_GUIDE.md, playwright.config.js.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 20:49:30 -05:00
varutasu
da50d78406
fix(security): drop wildcard CORS + redundant OPTIONS from 24 API routes (P0 #5)
Closes P0 #5 from PARTIAL to RESOLVED. Sweeps the remaining 24 pages/api/** handlers that carried the identical scaffolded wildcard-CORS + OPTIONS preflight pattern (Brief 4 cleaned login + register; this finishes the job). Adds a blocking forbidden-cors-headers CI job modeled on forbidden-endpoints to lock the cleanup against future regression. 25 files changed (+29/-261). Local: lint 128 baseline, vitest 21/21, zero CORS matches, YAML valid. CI: Playwright smoke 3/3 in 3.3s against post-removal preview (login/verify flow still works), new forbidden-cors-headers job passes in 4s, all gates green. PR #19 architect-commit ec22b70, implementer-commit a843736.
2026-05-24 20:41:38 -05:00
Randall Stillwell
eefc390636 docs: post-convoy cleanup for adopt-playwright-smoke
Reflects the merged adopt-playwright-smoke convoy (PR #18, squash
commit 7b6f751) in repo documentation. Closes the test-infrastructure
side of P1 #10 step 2 (launch sequence step 10). One commit in the
convoy: Brief 1, with two small lint-baseline-preserving deviations
from the brief's verbatim shape that the implementer report flagged.

.convoys/adopt-playwright-smoke.md:
  - frontmatter status: in-progress -> shipped (added shipped:
    2026-05-24)
  - new ## As-shipped section: operator-ratified Decisions (D1 keep
    .ts, D4 defer baselines, D6 simple scripts); two implementer
    deviations (removed unused eslint-disable-no-console directive
    that would have regressed lint 128 -> 129; placed @playwright/test
    first in devDeps for alphabetical correctness); cross-validation
    that smoke test 2 ("sign-in page renders") locks in PR #15's
    logged-out CTA work in components/Layout.js; empirical metrics
    from post-merge run 26376162598 (59s workflow, 3/3 in 2.9s,
    0-leak); operator-action-required note pointing at the queued
    seed-visual-baselines-on-linux follow-up; What did NOT change
    audit trail.

.convoys/ship-readiness.md:
  - Queued convoys: new entry seed-visual-baselines-on-linux (Linux-
    Docker baseline generation per Decision 4 + Boot-the-brief
    Finding 7; Mac-generated baselines would silently overwrite Linux
    CI baselines because the custom snapshotPathTemplate has no
    {platform} token).
  - Queued convoys: new RESOLVED block for adopt-playwright-smoke
    (PR #18, 7b6f751) — as-shipped surface, implementer deviations,
    empirical metrics (59s workflow / 3/3 in 2.9s / 0 secret leaks),
    the PR #15 cross-validation finding, operator-action-required
    going forward (the seed-visual-baselines-on-linux follow-up),
    flagged-but-deferred items, and ownership trail (3 architect-
    self-ratified decisions + 3 operator-ratified).
  - Launch sequence step 10: marked RESOLVED 2026-05-24 with the
    commit + metrics inline.
  - P1 #10 No tests Fix sequence: step 2 marked RESOLVED with the
    convoy + metrics ref; step 3 (re-enable test: job in
    ci.yml) called out as the next remaining task; step 5 (wire
    preview-smoke.yml to the Vercel preview URL) marked RESOLVED
    across PR #17 + PR #18 since both contributed.

AGENTS.md:
  - Section 6 Testing: rewritten end-to-end. Was "E2E/smoke runner
    still pending"; is now "@playwright/test@^1.60.0 wired, two
    projects (smoke + visual), npm run test:smoke / test:visual /
    test:visual:update". Documents the local-run convention (boot
    next dev separately, then BASE_URL=... npm run test:smoke);
    the one-time npx playwright install --with-deps chromium step;
    the no-baselines-yet state + the Linux-Docker seed command +
    the cross-platform mismatch reason (no {platform} token in
    snapshotPathTemplate); the CI behavior split (vitest blocking,
    smoke on every PR with pipeline:skip-smoke escape hatch,
    Screenshot diff path-filtered with the first-red-on-missing-
    baseline state documented). Updates vitest coverage count
    16 -> 21 (the +5 Layout regression-lock tests from PR #15).
    Notes TESTING_GUIDE.md is being eclipsed and will be renamed
    to docs/MANUAL_QA.md in a future cleanup convoy.
  - Section 7 Deployment: rewrites the Vercel-bypass paragraph from
    a single "query param now / header reserved for future" bullet
    into a two-shape audit ((1) query param on the wait-action's
    path: input per PR #17; (2) HTTP header in playwright.config.js's
    use.extraHTTPHeaders per PR #18). Documents the Decision-2
    fail-loud-in-CI / warn-in-dev predicate and references Gotcha #12
    as the established precedent (lib/rate-limit.js). Picked Section
    7 over a new Gotcha because the bypass plumbing is operationally
    a deployment concern, not an app pitfall.

No changes to: package.json, package-lock.json, playwright.config.js,
eslint.config.mjs, lib/**, pages/**, components/**, scripts/**,
.github/workflows/**, .cursor/rules/**, README.md,
tests/visual/homepage.spec.ts (JSDoc is already neutral-tense, no
future-tense references to clean up).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 19:33:43 -05:00
varutasu
7b6f7519b2
feat(test): adopt @playwright/test + ship playwright.config.js + visual scaffold (P1 #10 step 2)
Closes P1 #10 step 2. Adds @playwright/test@^1.60.0, playwright.config.js (CI fail-loud / dev warn-and-continue for missing VERCEL_AUTOMATION_BYPASS_SECRET, two projects partitioned by testMatch, snapshotPathTemplate aligned with workflow upload path), tests/visual/homepage.spec.ts (1 test, no baseline committed per architect Boot-the-brief Finding 7), 3 npm scripts, and 3 .gitignore entries. Smoke tests now run end-to-end against Vercel preview with x-vercel-protection-bypass header: 3/3 passed in 2.9s, total workflow 59s. Zero secret leaks in log. PR #18 architect-commit 3ac527e, implementer-commit c72d006.
2026-05-24 19:25:18 -05:00
Randall Stillwell
c2a43a0ce2 docs: post-convoy cleanup for fix-vercel-deployment-protection-in-ci
Reflects the merged fix-vercel-deployment-protection-in-ci convoy
(PR #17, squash commit 9a3e077) in repo documentation. Closes the
CI-infra side-effect of P0 #7. Three commits in the convoy: Brief 1
(bypass plumbing per spec), plus two scope expansions discovered
during CI validation (shell-injection hardening, and a Decision-A
shape correction to drop the cookie-bypass param).

.convoys/ship-readiness.md:
  - Queued convoys: mark fix-vercel-deployment-protection-in-ci as
    RESOLVED 2026-05-24 with 9a3e077. Document the 3-commit reality
    (365e9f0 Brief 1 bypass plumbing, b6f8688 shell-injection
    hardening of the gate Decide step, 043a6ee dropping
    &x-vercel-set-bypass-cookie=true), the empirical metrics
    (wait-action: 10-min timeout -> 194ms; workflow runtime:
    10+ min -> 59s), the documented expected red on Playwright
    smoke (npx playwright test fails because playwright.config.js
    doesn't exist yet -- adopt-playwright-smoke owns that), and the
    operator-rotation caveat (R6).

AGENTS.md:
  - Section 7 Deployment: correct the noun "header" -> "query param
    on wait-action's path:" since that's what actually landed per
    Decision A. Also document the without-cookie form (the cookie
    variant 401s through axios's missing cookie jar) and the
    operator re-seed runbook for token rotation.
  - Section 7 Deployment: fold in a one-liner about the GitHub
    Actions ${{ }}-in-shell-is-injection-vector pattern, with the
    env: + quoted-shell fix shape. Picked Section 7 over a new
    Gotcha #13 because the existing Gotchas list is dominated by
    app-level pitfalls (auth, SQL clients, ESLint), and CI YAML
    hardening is naturally co-located with deployment.

.convoys/fix-vercel-deployment-protection-in-ci.md:
  - frontmatter status: in-progress -> shipped (added shipped:
    2026-05-24)
  - new ## As-shipped section: 3-commit reality, Decision-A shape
    deviation (we shipped without &x-vercel-set-bypass-cookie=true),
    empirical timings (194ms wait, 59s total), remaining-red
    attribution to adopt-playwright-smoke, and the
    operator-rotation caveat.

No changes to: package.json, lib/**, pages/**, components/**,
scripts/**, .github/workflows/**, .cursor/rules/**, README.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 16:32:45 -05:00
varutasu
9a3e077d40
fix(ci): plumb VERCEL_AUTOMATION_BYPASS_SECRET into preview-smoke + visual-diff (#17)
* architect: plan fix-vercel-deployment-protection-in-ci convoy (1 brief)

Flip convoy status queued -> in-progress and append the architect
output per the role-architect contract:

- Decisions (post-IA round) A/B/C/D with recommendations + rationale,
  routed back to operator for gate-1 ratification (A and D especially).
- Architecture section: file plan, risk list (R1-R8), test plan,
  decomposition (1 brief, justified), slice_dependencies block.
- brief-1: inject VERCEL_AUTOMATION_BYPASS_SECRET into both workflows
  via query-param-on-path (Decision A), tighten max_timeout 600 -> 120
  (Decision B), extend gate to skip fork PRs (Decision D), and forward
  the secret as an env var to the Playwright/visual step for forward-
  compat with adopt-playwright-smoke.

Boot-the-brief findings preempted in the brief:
- wait-for-vercel-preview@v1.3.2 source confirms only `targetUrl` (the
  bare deployment URL) is ever logged or emitted as outputs.url; the
  bypass query in `path:` is structurally invisible to logs and to
  downstream consumers of ${{ steps.vercel.outputs.url }}.
- The action exposes no custom-header input -- Option B is mechanically
  impossible for the wait step without forking the action.
- `path:` MUST begin with a leading `/` because the action parses it
  via `new URL(path, url)`.
- PR #16 run logs confirm the 401 timeout failure mode the convoy targets.
- Playwright config doesn't exist yet (adopt-playwright-smoke owns it);
  this brief only plumbs the env var.

No workflow YAML, no Playwright config, no AGENTS.md changes in this
commit -- those land in the implementer phase per the architect contract.

Awaiting human gate 1 (Decisions A + B + D ratification + brief approval)
before implementer dispatch.

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

* fix(ci): plumb VERCEL_AUTOMATION_BYPASS_SECRET into preview-smoke + visual-diff (Brief 1 of fix-vercel-deployment-protection-in-ci)

Closes the CI-infra half of P0 #7's follow-up. PR #16 (squash commit
7e97254) added scoped permissions to both workflows but exposed that
Vercel Deployment Protection 401s anonymous GitHub-runner requests,
causing both Playwright smoke and Screenshot diff to time out at 10
minutes on every PR. This brief plumbs the bypass secret end-to-end
so the wait-action's healthcheck reaches 200.

Per architect Decision A (.convoys/fix-vercel-deployment-protection-in-ci.md):
- preview-smoke.yml + visual-diff.yml: wait-for-vercel-preview's
  `path:` input now carries the bypass as a query parameter
  `?x-vercel-protection-bypass=${{ secrets.* }}&x-vercel-set-bypass-cookie=true`.
  The action only logs the bare targetUrl (verified in action.js:357,360,363)
  so the secret stays out of workflow logs.

Per Decision B:
- max_timeout: 600 -> 120. PR #16 evidence shows Vercel previews are up
  within seconds of job start; 120s gives ample headroom and surfaces
  misconfigurations in ~2 minutes instead of ~10.

Per Decision D (NEW -- surfaced by Boot-the-brief):
- gate: job's Decide step now checks github.event.pull_request.head.repo.fork
  FIRST. Forks lack repo secrets, so they would otherwise burn ~4 minutes
  per PR on a misleading 401. The fork-check emits `::notice::` and short-
  circuits before the existing skip-via-PR-body directive runs.

Forward-compat for adopt-playwright-smoke:
- Both workflows' Playwright/screenshot-capture step now exports
  VERCEL_AUTOMATION_BYPASS_SECRET as env. The actual Playwright config
  consumes it via extraHTTPHeaders in adopt-playwright-smoke's brief.
  BASE_URL stays as the bare \${{ steps.vercel.outputs.url }} (no query
  string) so it remains safe to echo.

Verification:
- YAML parses (js-yaml load on both files: preview-smoke jobs [gate, smoke];
  visual-diff jobs [gate, visual])
- actionlint not run (binary not installed locally); recommend installing
  for future PRs. Future adopt-actionlint convoy could add it to CI.
- No `set -x`, `echo`, `cat`, or `printf` of the secret or bypass URL
  in any modified step
- permissions: and concurrency: blocks unchanged (PR #16 contracts preserved)

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

* fix(ci): route github.event.pull_request.body through env: to avoid shell injection

Pre-existing latent bug surfaced by PR #17's CI run. The Decide step's
inline `${{ github.event.pull_request.body }}` interpolation pastes
arbitrary PR-body text directly into a bash script. When the body
contains shell metacharacters (parens, pipes, backticks, redirections,
etc.) the resulting script either errors out at YAML-load time OR —
worse — executes attacker-controlled shell.

This bit PR #17 with a real syntax error ("unexpected token `('")
because the PR body contains parenthesized phrases like
"(was: 10-minute timeout)". Every Decide-step run in this repo has
been one badly-formatted PR body away from breaking the gate.

Fix: forward `github.event.pull_request.body` and
`github.event.pull_request.head.repo.fork` through the step's `env:`
block as `PR_BODY` and `PR_IS_FORK`, then quote them in shell
(`"$PR_BODY"`, `"$PR_IS_FORK"`). The env-var path leaves the values
as plain strings rather than syntactically embedded code, which is the
standard GitHub Actions hardening pattern (see GitHub's "Security
hardening for GitHub Actions" → "Using a third-party action").

Same change in both workflows; ~9 LOC each.

This fix is technically beyond Brief 1's scope (which targeted only
Vercel-bypass plumbing) but is added in this convoy because the bug
actively blocks Brief 1's success criterion from being validated on
PR #17. Documented in the convoy file's "Anything flagged but not
acted on" follow-up pass.

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

* fix(ci): drop x-vercel-set-bypass-cookie=true from wait-action path

The wait-action's healthcheck was still 401ing despite the bypass query
being correct. Root cause: with `x-vercel-set-bypass-cookie=true`, Vercel
returns 307 + Set-Cookie (`_vercel_jwt`), but axios in Node has no cookie
jar — it follows the redirect to the bare URL without the cookie, which
then 401s.

Local verification (run by operator):
  curl -sI "https://<preview>/?x-vercel-protection-bypass=<secret>" | head -1
  → HTTP/2 200   (works, no cookie needed)

  curl -sI "https://<preview>/?x-vercel-protection-bypass=<secret>&x-vercel-set-bypass-cookie=true" | head -1
  → HTTP/2 307   (the redirect-without-cookie path that breaks axios)

For a one-shot healthcheck, the per-request bypass query is enough.
The cookie variant stays reserved for the future Playwright config
(adopt-playwright-smoke) where a real browser cookie jar exists.

Added an inline comment in preview-smoke.yml explaining this so the
next agent doesn't accidentally re-add the cookie param.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 16:26:22 -05:00
Randall Stillwell
efca3e84c1 docs: mark P0 #8 (Next.js bump) RESOLVED in ship-readiness.md
Doc-drift fix discovered after the fix-layout-default-user cleanup
pass. The bump-next-js convoy shipped on 2026-05-23 as commit
e57ea17, and the commit body explicitly states "Closes P0
ship-blocker #8" — but no dedicated doc-writer pass ever ran for
that convoy, so .convoys/ship-readiness.md still had P0 #8 listed
as open even though Vercel has been deploying main + every PR
successfully since.

Patches only P0 #8 to RESOLVED with the same shape used by P0
#1, #2, #3, #4, #7:
  - Title: "— **RESOLVED 2026-05-23**"
  - Resolved by: bullet citing e57ea17
  - As-shipped: 7-point summary of the bump's actual deliverables
    (next 15.4.3->16.2.6, ESLint v9-fallback, typescript devDep,
    eslint.config.mjs, scripts.lint update, images.remotePatterns
    migration, build verification)
  - Side-effects deferred: bump-react, bump-eslint-10, bump-typescript-6,
    fix-lint-baseline, App Router migration
  - Doc drift note acknowledging this entry was added ~24h late

No code changes. No package.json / lib/** / pages/** / components/**
touched. AGENTS.md was already current (Gotchas #9, #10, #11 already
reflect Next 16 + ESLint v9 + Turbopack post-bump state).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 14:40:46 -05:00
Randall Stillwell
b7ddd0895e docs: post-convoy cleanup for fix-layout-default-user (+ queue CI-protection follow-up)
Reflects the merged fix-layout-default-user convoy (PR #15) and the
companion CI permissions fix (PR #16) in repo documentation. Also
queues the new fix-vercel-deployment-protection-in-ci convoy that
PR #16 exposed.

.convoys/ship-readiness.md:
  - P0 #7: mark RESOLVED 2026-05-24 with squash commit ca302a8.
    Document the as-shipped Layout default-null change, the 7-page
    sweep, the 5 new regression-lock vitest assertions, and the
    queued follow-ups (single-auth-provider, MobileNavigation
    cleanup) that stayed explicitly out of scope.
  - Queued convoys: add fix-vercel-deployment-protection-in-ci
    (P2, CI infra) — PR #16's permissions fix exposed that Vercel
    Deployment Protection 401s anonymous CI requests; needs a
    bypass-secret plumb to land cleanly. New section also captures
    other in-flight follow-ups (rotate-default-admin, cors-tighten,
    add-rate-limiting, purge-weak-creds-from-helpers,
    single-auth-provider, cleanup-mobile-nav-dead-props,
    bump-eslint-10) so the audit trail is centralized.

AGENTS.md:
  - § 4 Gotcha #8: mark RESOLVED with commit ref ca302a8. Mirror
    the convention used by the prior cleanup commits for #2, #3,
    #4, #5 (entry kept, not renumbered).
  - § 3 Key conventions: add a new "Layout user prop" bullet
    documenting the new default-null + logged-out-CTA contract so
    the convention is discoverable from the conventions list, not
    just the resolved-gotcha entry.

.convoys/fix-vercel-deployment-protection-in-ci.md (new):
  - Queued scaffold. Operator must seed
    VERCEL_AUTOMATION_BYPASS_SECRET as a repo secret before the
    implementer can run. Decisions to ratify (query param vs.
    header), known constraints, acceptance criteria, and
    out-of-scope all enumerated.

No changes to: package.json, lib/**, pages/**, components/**,
scripts/**, .github/**, README.md, .cursor/rules/**.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 14:37:39 -05:00
varutasu
ca302a89c1
fix(layout+pages): default user=null + page audit sweep (P0 #7) (#15)
* convoy: scope fix-layout-default-user (P0 #7 — Layout maintainer-email leak)

The last remaining P0 ship-blocker from .convoys/ship-readiness.md.
components/Layout.js line 562 defaults the user prop to a real email
address (me@randallstillwell.com); any page that renders Layout without
passing user explicitly impersonates the maintainer.

Scope: components/Layout.js + audit of 17 pages that import Layout
(grep-confirmed list in convoy file). Single PR likely. Auditor cohort
skipped (no design-system, IA, or browser-smoke surface).

Architect to address:
  - Q1: logged-out rendering branch design (navbar, mobile-nav,
        auth-only items treatment)
  - Q2: page audit triage into always-auth / public-or-auth /
        anonymous-allowed buckets
  - Q3: brief decomposition (single brief / 2 briefs in 1 PR / fan-out)
  - Q4: whether to add vitest coverage for the logged-out branch
        (recommend yes — small surface, high regression protection)

Hard out-of-scope: branding (pick-a-name), auth-provider collapse
(single-auth-provider), Layout god-component split (god-component-split).

depends_on: bump-next-js (shipped), fix-auth-bypass (shipped),
            drop-public-setup (shipped)
addresses: P0 #7 from .convoys/ship-readiness.md
parent: ship-readiness

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

* architect(fix-layout-default-user): plan + briefs 1-2 (Layout fix + page audit)

2 briefs, single PR. ~12 files net (down from the 18 in the original scope —
10 of the 17 Layout-importing pages already pass user explicitly).

Brief 1: components/Layout.js default user=null + Sign-in CTA branch in
  UserProfileDropdown when logged out. Adds first jsdom test in the repo
  at test/components/Layout.test.js (Decision D2) with 5 regression-lock
  assertions. devDeps: jsdom@^29, @testing-library/react@^16.

Brief 2: page audit sweep — 7 pages need code changes:
  - Pass user={user} to Layout: scanner.js, deck-builder.js (×4),
    deck/[id].js (×3), decks.js (×3)
  - Replace page-level useState({email: 'me@...'}) → useState(null) +
    null-guards: profile.js, settings.js
  - Replace hardcoded const user = {email: 'me@...'} with useAuth():
    card/[id].js

Discovered second anti-pattern: profile.js, settings.js, card/[id].js
seed page-level state with the maintainer email. Folded into Brief 2 since
success metric "no real email address remains in any component default-prop"
reads naturally to include page-level seed values.

Decisions:
  A1 — Sign-in CTA replaces avatar+email+dropdown when user===null;
       hides auth-only dropdown (Profile/Settings/Logout/Admin);
       keeps public + community nav visible
  B  — Per-page bucket assignment (10 already correct, 7 need fix);
       full per-page table with justification in convoy file
  C2 — Two briefs in one PR (Brief 1 = Layout + test; Brief 2 = page
       sweep depends on Brief 1). C1 buries the conceptual change under
       mechanical edits; C3 is over-orchestrated for this scope
  D2 — vitest lock-in; first jsdom test in repo; same negative-regression
       style as test/lib/permission-middleware.test.js (synthetic-admin
       shape). devDeps jsdom + @testing-library/react

Risks tracked R1-R8. Biggest: R2 (useState(null) null-deref in 3 leaky
pages — mitigated by audit-pass mandate + manual smoke).

MobileNavigation deliberately NOT folded in: its user prop is dead code
(never reads user.*); different bug class; cleanup queued separately to
avoid scope expansion.

Flagged-but-deferred:
  - 4 pages still import useAuth from lib/auth-context.js
    → single-auth-provider (queued P1 #9)
  - Layout headers still render "Deck Hearth" / "DH" branding
    → pick-a-name (queued P1 #12)
  - MobileNavigation dead user prop → cleanup-mobile-nav-dead-props
    or fold into god-component-split

addresses: P0 #7 from .convoys/ship-readiness.md (last P0 ship-blocker)
parent: ship-readiness
Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(layout): default user=null + Sign-in CTA when logged out (Brief 1 of fix-layout-default-user)

Closes the source-side half of P0 #7 from .convoys/ship-readiness.md.
The page-side sweep (Brief 2) follows in a separate commit.

components/Layout.js:
  - Default user prop is now null (was hardcoded to
    { email: 'me@randallstillwell.com', role: 'user' })
  - UserProfileDropdown renders a "Sign in" link to /login when
    user === null instead of the maintainer's email + auth-only menu
    items (Decision A1)
  - All user.* accesses guarded with optional chaining or null checks
  - useState hook stays above the new null-user early return to satisfy
    rules-of-hooks (boot-the-brief caught this on the first try;
    see AGENTS.md Gotcha #11.5)

test/components/Layout.test.js (new):
  - First jsdom test in the repo (Decision D2)
  - 5 regression-lock assertions: no maintainer email ever rendered
    (prop omitted, prop=null), Sign-in link exists with href=/login,
    supplied email renders when prop is set, no "Guest" placeholder
    (locks A1 copy choice)
  - Mocks next/link, next/router (prefetch, replace, events, query),
    and theme-context.useTheme for jsdom safety under Next 16

package.json + package-lock.json:
  - Add jsdom@^29 and @testing-library/react@^16 to devDependencies
  - @testing-library/dom@^10 added explicitly (peer auto-install
    skipped it under npm 11; brief anticipated this fallback)

vitest.config.js (deviation from brief — see PR description):
  - Add esbuild { loader: 'jsx', jsx: 'automatic' } so vitest can
    parse JSX in .js files. Required to import any React component
    written in the repo's Next.js pages-router .js convention
    (AGENTS.md Gotcha #9). The brief said "no change" to this file,
    but JSX-in-.js parsing is a hard prerequisite for the new test
    to import components/Layout.js — the alternatives (rename test
    to .test.jsx; rewrite test in React.createElement) either break
    the test glob or still hit the same Layout.js parse failure.
    Other tests are unaffected (they import non-JSX modules).

Smoke output: see PR description.

addresses: P0 #7 from .convoys/ship-readiness.md (last P0 ship-blocker)
Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(pages): pass user explicitly + null-guard leaky page seeds (Brief 2 of fix-layout-default-user)

Closes the page-side half of P0 #7 from .convoys/ship-readiness.md.
Brief 1 (commit ddf8fd2) handled the Layout-side fix.

Per the architect's per-page bucket table (Decision B in
.convoys/fix-layout-default-user.md), 7 pages needed code changes;
the other 10 of 17 Layout-importing pages already pass `user` correctly.

Pass user={user} to Layout (4 pages, 11 call sites):
  - pages/scanner.js (1 call)
  - pages/decks.js (3 calls)
  - pages/deck-builder.js (4 calls)
  - pages/deck/[id].js (3 calls)
  (All four still import useAuth from lib/auth-context.js — that's
   intentional and stays as-is until the single-auth-provider convoy
   collapses the three parallel auth surfaces.)

Replace leaky page-level seed values with useState(null) + null guards
(2 pages, R2 mitigation):
  - pages/profile.js: useState({email: 'me@...', role: 'user', ...})
                     → useState(null) + ?. on every sync user.* read
                     + early-return guards in getDisplayName/getInitials
                     + conditional render around the "Member since" block
                       so formatDate(undefined) never runs
  - pages/settings.js: same pattern (single user.email reader guarded)

Replace hardcoded const with useAuth from lib/use-auth.js (1 page):
  - pages/card/[id].js: const user = {email: 'me@...'}
                       → const { user } = useAuth() (called unconditionally
                       at the top of the component; rules-of-hooks safe)

Verification:
  - grep 'me@randallstillwell.com' pages/ → 0 hits
  - 21/21 vitest tests pass (16 pre-existing + 5 from Brief 1)
  - npm run lint matches baseline (128 problems pre, 128 post; verified
    via git stash before/after)
  - Manual static read-through of every diff; ReadLints clean on the 7
    files
  - Dev-server smoke: /cards anonymous returned HTTP 200 with 0
    'me@randallstillwell' matches before the user's shared dev server
    became unresponsive mid-session (same dev-server-shared-by-user
    constraint flagged in Brief 1); interactive logged-in smoke is
    parent/operator gated

Flagged-but-deferred (untouched per scope):
  - 4 pages still import useAuth from lib/auth-context.js
    → single-auth-provider (queued P1 #9)
  - components/MobileNavigation.js still receives dead user prop
    → cleanup-mobile-nav-dead-props (or fold into god-component-split)

addresses: P0 #7 from .convoys/ship-readiness.md (last P0 ship-blocker)
Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 14:31:37 -05:00
varutasu
7e972546b7
fix(ci): scoped permissions for preview-smoke + visual-diff workflows (#16)
Both Playwright-on-Vercel workflows fail at the very first action
(`patrickedqvist/wait-for-vercel-preview@v1.3.2`) with 403 "Resource
not accessible by integration". The cause is the repo-default workflow
token being read-only-by-default with no scopes declared by the workflow.

Add minimal scoped permission blocks per the GitHub Actions least-privilege
guidance:

  preview-smoke.yml:
    contents: read
    deployments: read   # wait-for-vercel-preview queries GitHub Deployments
    pull-requests: read # correlate deployment with this PR
    statuses: read      # some Vercel deployments use commit statuses

  visual-diff.yml:
    contents: read
    deployments: read
    pull-requests: write # final step posts "Visual Diff" comment via Issues API
    statuses: read

Verified against the failure on PR #15:
  - Playwright smoke: "Resource not accessible by integration" on
    wait-for-vercel-preview → fixed by deployments+statuses+pull-requests:read
  - Screenshot diff: 403 from POST /repos/.../issues/15/comments with
    `x-accepted-github-permissions: issues=write; pull_requests=write`
    in the response → fixed by pull-requests:write (covers Issues API
    for PR comments; issues:write would also work but pull-requests:write
    is the idiomatic scope)

Unblocks visual-regression signal on every future PR. No code changes,
no test changes — workflow YAML only.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 14:22:12 -05:00
Randall Stillwell
eeb14eb0e0 docs: post-convoy cleanup for drop-public-setup
Reflects the merged drop-public-setup convoy (PR #13) in repo
documentation. Small focused pass; no behavior changes.

AGENTS.md:
  - § 1 auth bullet: replace "seed admin row still ships in
    setup-neon-db.js" claim with the new env-var-gated reality and
    the R1 operator-rotation caveat.
  - § 4 Gotcha #4: mark RESOLVED with commit refs (ff80753 + b63b509),
    document the as-shipped behavior, the Brief 2 CJS→ESM Node 22.x
    fix, and the R1 operator caveat. Entry kept (not renumbered) per
    the same convention used for resolved gotchas #2, #3, #5.
  - § 5 Running locally: add ADMIN_INITIAL_PASSWORD to the env-var
    template list with a note that setup-db exits 1 if it's unset.

.convoys/ship-readiness.md:
  - P0 #3: mark RESOLVED 2026-05-23 with commit refs, document the
    full as-shipped behavior including Brief 2's CJS→ESM bonus,
    the R1 operator caveat (Decision A — going-forward only),
    and the deferred sibling weak-cred references queued for
    purge-weak-creds-from-helpers.

.cursor/rules/no-go-zones.mdc:
  - Editing rules of thumb: clarify the schema-vs-operational
    distinction for scripts/setup-neon-db.js. drop-public-setup
    set the precedent that operational changes (env-var gating,
    pre-flight validation, module-system fixes) are allowed in
    place, while DDL changes still need a separate migration
    script. Documented so future agents don't have the same
    confusion the drop-public-setup architect did (see Decision B
    in .convoys/drop-public-setup.md).

No changes to: package.json, lib/**, pages/**, components/**,
scripts/**, .github/**, README.md (already updated in PR #13).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 17:06:35 -05:00
Randall Stillwell
b63b5090cc fix(seed): convert scripts/setup-neon-db.js from CJS to ESM (Node 22.x compat)
Brief 2 of drop-public-setup. Closes Decision D — pure module-system
conversion of scripts/setup-neon-db.js so `npm run setup-db` actually
runs on Node 22.x where package.json has "type": "module" (added by
bump-next-js for ESLint v9 flat-config support).

Before this commit, `npm run setup-db` threw:
  ReferenceError: require is not defined in ES module scope

After this commit, brief 1's ADMIN_INITIAL_PASSWORD env-var gate actually
fires as documented.

Changes (all in scripts/setup-neon-db.js):
  - require('dotenv').config(...) → import dotenv + dotenv.config(...)
  - require('@neondatabase/serverless') → import { neon }
  - inline require('bcryptjs') hoisted to top-of-file import bcrypt
  - no functional changes; same DDL, same env-var gate, same console.logs

Smoke verification: see PR description.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 17:02:44 -05:00
Randall Stillwell
792439b5eb architect(drop-public-setup): expand scope with brief 2 (CJS→ESM)
Mid-convoy discovery: brief 1's implementer confirmed scripts/setup-neon-db.js
does not actually run on Node 22.x because bump-next-js added "type": "module"
to package.json but the seed script still uses CJS require() calls.
Throws ReferenceError immediately on `npm run setup-db`.

Architect's original "Anything flagged but not acted on" #1 claim that "it
runs successfully today under Node 22" was incorrect for Node 22.14.0.

Decision D (ratified by user 2026-05-23): expand convoy to include brief 2
rather than queue a separate convert-setup-db-to-esm follow-up. Rationale:
brief 1's env-var gate is theatrical security on a script no operator can
execute; the CJS→ESM conversion is mechanical (~6 LOC, same file, no
functional changes); splitting into two convoys creates a regression window
where operators on Node 22.x cannot bootstrap a database.

Brief 2 scope: pure module-system conversion in scripts/setup-neon-db.js:
  - require('dotenv').config(...) → import dotenv + dotenv.config(...)
  - require('@neondatabase/serverless') → import { neon }
  - inline require('bcryptjs') hoisted to top-of-file import
  - no functional changes; same DDL, same env-var gate, same console.logs

Verification: smoke must now show npm run setup-db actually executes (no
ReferenceError); brief-1 env-var gate must still fire as documented;
all 16 vitest tests must still pass.

Updated:
  - .convoys/drop-public-setup.md Decomposition (brief 2 added, depends_on brief 1)
  - .convoys/drop-public-setup.md slice_dependencies YAML
  - .convoys/drop-public-setup.md § Decisions (added Decision D)
  - .convoys/drop-public-setup.md § Anything flagged but not acted on
    (item #1 marked resolved by brief 2)

addresses: P0 #3 from .convoys/ship-readiness.md + Node 22.x compat
parent: ship-readiness
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 17:02:44 -05:00