Follow-up PR to #95 (Liquid Glass foundation + primitives + Layout shell) that closes out the remaining sub-convoy briefs in a single sweep. Operator-instructed scope: "finish off the design changes." After this PR, **all 8 Liquid Glass sub-convoys are MERGED to main**; the deferred-from-#5 `fix-card3d-state` convoy is dropped (its target, `components/Card3D.js`, turned out to be dead code). ## #2 Brief 2 — Remaining 8 modals migrated to <Modal> primitive - `CollectionsSuccessModal.js` — wrap in <Modal hideCloseButton>; 2 Buttons. - `CollectionsEditModal.js` — full <Modal> + <Input> + <Button> rewrite (4 fields, tag chip section, public-toggle preserved, 2 footer Buttons). - `CollectionEditModal.js` — same pattern as above (4 fields + public-toggle + 2 Buttons). - `CardDetailDeckModal.js` — <Modal> + native select (Select primitive not in scope) + 2 Buttons; sweep `gradient-bg-purple` → `<Button variant="primary">`. - `UploadImageModal.js` — <Modal> + token-driven URL/file tab switcher + drag-drop using `--accent-ember` rim + 2 Buttons (one with `loading` prop). - `CollectionSelectionModal.js` — largest of the set (header summary + SearchBar + scrollable list w/ checkbox toggles + footer); migrated to <Modal size="lg"> while preserving the per-collection card preview thumbnails. - `OCRSettings.js` — trivial <Modal> wrap + single primary <Button>. - `pages/decks.js` — both inline modals (Create Deck + Edit Deck) and `components/ScannerPageView.js` (Create List) migrated; ScannerPageView dropped its `useFocusTrap` named-import (Modal's internal focus trap owns the panel ref now). - **`.github/workflows/ci.yml` `forbidden-modal-shell-without-primitive`** — grandfather list emptied to zero entries; gate is now strict. ## #3 Brief 2 — Forms migrated to <Button> / <SearchBar> - `pages/dashboard.js` — 3 CTAs → <Button> (Create List with leadingIcon, Create Your First List, View All Lists). - `pages/my-cards.js` — empty-state CTA → <Button variant="primary" size="lg">. View-mode toggle buttons intentionally left native (icon-only, doesn't match Button variants). - `pages/community/collections.js` — Go to My Lists CTA → <Button>. - `components/CollectionsPageView.js` — Discover Community + Create List header CTAs → <Button>; search input → <SearchBar>. - Card-grid per-row icon buttons (CollectionsPageView, my-cards, CardsPageView) intentionally left native — tiny per-card actions whose styling doesn't match Button variants and would invalidate visual-diff baselines. ## #5 — scope revised + landed `components/Card3D.js` deletion: surveyed every importer with grep — **zero consumers** in `pages/**` or `components/**`. Only references were in convoy docs. The "pre-existing state-management bug" (state setters used without useState declarations) never affected the running app because the component was never rendered. -505 LOC. The `fix-card3d-state` convoy is dropped from the roadmap as a result. The actual card-grid component (`components/CardItem.js`) is intentionally **not** modified in this sweep — it has per-rarity glow tuning that the existing visual-diff baseline locks in, and the architect's #5 deferral note specifically called out the dedicated baseline re-seed cost. A future implementer turn can apply rim-light tokens to CardItem with its own baseline re-seed when an operator wants that polish. ## #6 Brief 1 — Landing + invite pages glass-migrated - `pages/index.js` — top nav: `var(--glass-surface-mid)` + `--glass-blur-mid` + rim-light. 3 feature cards: `<GlassSurface tint="mid" rim="subtle" elevation="ambient">`. Featured-list cards (the public collection grid): same `<GlassSurface>` recipe with motion-token transitions. All 6 CTA buttons → <Button variant="primary"|"secondary"|"ghost"> with proper sizes. Pulse-loading placeholders tagged `.motion-essential` so reduced-motion users still see them animate (state-meaningful). - `pages/invite/accept.js` + `pages/invite/decline.js` — both outcome panels wrapped in `<GlassSurface tint="mid" rim="subtle" elevation="pronounced">`. Loading spinner border colors corrected from `--text-accent` (which didn't exist) to `--accent-ember`. All 8 buttons → <Button>. `gradient-bg-ember` consumers retained (the canonical warm-palette utility class is fine). ## #8 Brief 2 — Legacy alias sweep + CI gate graduation - Swept `gradient-bg-purple` → `gradient-bg-ember` across **8 files** / **13 occurrences**: `CardDetailQuantityModal`, `CardEditorView`, `CardEditorForm`, `AdminProtected`, `pages/card/[id]`, `pages/invite/{accept,decline}`, `pages/admin/card-import`. `gradient-bg-purple` was a dangling class name with no CSS definition (it was rendering no styling), so the sweep is also a bug fix — those buttons now actually get the ember gradient. - Deleted the 5 dead CSS classes from `styles/globals.css`: `.gradient-text-blue`, `.gradient-text-purple`, `[data-theme="dark"] .glow-blue`, `[data-theme="dark"] .glow-purple`, `[data-theme="dark"] .glow-pink`. Each was zero-consumer post-sweep. - **Graduated the `forbidden-deprecated-color-aliases` CI job from WARN to FAIL.** All 9 patterns (`gradient-text-{purple,pink,blue}`, `glow-{purple,pink,blue}`, `gradient-bg-{purple,blue,pink}`) now block the build if any consumer is reintroduced. ## Verification (local + CI gates locally exercised) - Lint: 0 errors, 2 pre-existing warnings (`CardEditorForm.js` + `CollectionsPageView.js` carry-overs from before #95; out of scope). - Vitest: 104/104 passing — unchanged from #95. - Build: clean (Turbopack default; passes both light + dark theme prerender). - `forbidden-modal-shell-without-primitive` gate: locally clear (`grep -lE 'fixed inset-0 bg-black bg-opacity-' pages components -r --include='*.js'` returns no matches). - `forbidden-deprecated-color-aliases` gate: locally clear (all 9 patterns return no matches in `pages/` or `components/`). ## What still needs human action - **Linux visual-diff baselines** must re-seed via the Docker workflow in `AGENTS.md` § 6. This PR's landing-page + invite-page changes will produce baseline drift on the homepage screenshot (which is currently the only baseline committed) AND additional baselines will be generated for the landing's glass-card sections once the visual spec is expanded. Recommended: run the Docker re-seed against this PR's Vercel preview, commit the result to this branch, push, verify CI green, then merge. - Vercel auto-promotes the merge to production. ## Closes / supersedes - Closes `.convoys/liquid-glass-modal-and-surface-primitive.md` Brief 2 (status → merged). - Closes `.convoys/liquid-glass-form-primitives.md` Brief 2 (status → merged with explicit per-row-icon-button deferral note). - Closes `.convoys/liquid-glass-public-and-auth.md` Brief 1 (status → merged). - Closes `.convoys/cleanup-legacy-design-css.md` Brief 2 (status → merged + CI gate FAIL). - Drops `.convoys/liquid-glass-card-surfaces.md` Brief 1 prerequisite (`fix-card3d-state` no longer needed; Card3D deleted). - Drops the queued `fix-card3d-state` follow-up from the roadmap (target deleted). - Updates `.convoys/ship-readiness.md` § "Design-system redesign portfolio" with a "Finish-portfolio sweep" subsection documenting final status of all 8 sub-convoys. Co-authored-by: Cursor <cursoragent@cursor.com>
106 KiB
| name | classification | success_metric | skip | status | created |
|---|---|---|---|---|---|
| ship-readiness | epic | tcg-vault is safe to expose to anonymous internet traffic with a documented launch checklist green | open | 2026-05-22 |
Ship-readiness convoy
Umbrella convoy capturing the full agent-pipeline review of tcg-vault as of 2026-05-22. Findings are grouped by L2 role lens (Reviewer / Architect / Design-system / A11y / IA / Doc-writer) and severity. Each item points to the convoy that will execute the fix.
Code graph: 122 files, 628 nodes, 5602 edges, 11 communities. Indexed by user-code-review-graph MCP.
Status summary (as of 2026-05-24)
P0 ship-blockers: 8 of 8 RESOLVED. Launch-readiness P0 checklist is empty.
| Item | Status | Convoy |
|---|---|---|
P0 #1 — getUserFromRequest hardcoded admin |
RESOLVED 2026-05-23 | fix-auth-bypass Brief 2 (258e479) |
P0 #2 — JWT_SECRET hardcoded fallback |
RESOLVED 2026-05-23 | fix-auth-bypass Brief 1 (4a10dce) |
| P0 #3 — Default admin credentials in seed | RESOLVED 2026-05-23 | drop-public-setup (ff80753 + b63b509) |
| P0 #4 — Dev-only test endpoints | RESOLVED 2026-05-23 | fix-auth-bypass Brief 3 (fc0dd73) |
| P0 #5 — Wildcard CORS on API surface | RESOLVED 2026-05-24 | fix-auth-bypass Brief 4 (297afca) + cors-tighten (da50d78) |
| P0 #6 — No rate limiting | RESOLVED 2026-05-24 | fix-auth-bypass Brief 4 (297afca, login + register) + add-rate-limiting (708ef45, the remaining surface) |
| P0 #7 — Layout default-prop leaks email | RESOLVED 2026-05-24 | fix-layout-default-user (ca302a8) |
| P0 #8 — Next.js 15.4.3 vulnerable version | RESOLVED 2026-05-23 | bump-next-js (e57ea17) |
Milestone reached 2026-05-24: add-rate-limiting (PR #20, squash
commit 708ef45) closed P0 #6 — the last open P0 — flipping the
ship-blocker set from 7/8 to 8/8 RESOLVED. The security gate is
closed; P1 quality bar is 6/6 RESOLVED (lint baseline cleared 2026-06-02).
Remaining launch work is P2 / P3 polish in this file's Queued convoys section.
None of those are P0 ship-blockers.
P1 quality-bar: 6 of 6 RESOLVED (last item closed 2026-06-02). The 7-convoy multitask wave merged 2026-05-26 (PRs #26–#32) closed P1 #8, #9, and #11:
- P1 #8 single-sql-client → RESOLVED 2026-05-26 (PR #30, squash
c403ea4). - P1 #9 single-auth-provider → RESOLVED 2026-05-26 (PR #31, squash
0668b0c). - P1 #11 migration-tool → RESOLVED 2026-05-26 (PR #32, squash
de9f334).
Combined with prior closures (P1 #10 via adopt-vitest + adopt-playwright-smoke
PR #18; P1 #12 via pick-a-name PR #21) and P1 #11.5 fix-lint-baseline
(PRs #61–#63, 2026-06-02), the P1 lane is complete. Lint is 0 problems;
CI lint is blocking (no || true / continue-on-error).
P0 — ship-blockers (security)
These MUST land before any anonymous traffic touches the production URL.
1. getUserFromRequest returns a hardcoded admin when no Bearer token is present — RESOLVED 2026-05-23
- Resolved by:
fix-auth-bypassBrief 2, commit258e479(PR #8). Follow-up Brief 6 hotfix1fca3aaadded explicit 401 guards to the cards-collection POST/PUT/DELETE branches that previously masked the bug as 500s. - File:
lib/permission-middleware.jslines 13-17. - Impact: Every API route that calls
getUserFromRequest(30+ handlers — seeuser-code-review-graphcross-community edges fromapi-handler→lib-admin) accepts unauthenticated requests as admin user 1. - Repro:
curl https://<host>/api/collectionswith noAuthorizationheader returns admin's collections. - Fix: Delete lines 13-17. Return
nullwhen no Bearer token. Update every caller to handlenullproperly (most already do; the broken fallback was masking the right path). - As-shipped: The helper now returns
nullfor any unauthenticated request. 16 unit tests intest/lib/permission-middleware.test.jslock in the contract (including a negative regression against the old synthetic-admin shape).pages/api/auth/verify.jsreturns 401 on the no-token branch instead of fetching the seed admin row. - Owns:
role-architect+role-implementer(one PR; small surface area in the helper, callers already check!user).
2. JWT_SECRET hardcoded fallback in 7 files — RESOLVED 2026-05-23
- Resolved by:
fix-auth-bypassBrief 1, commit4a10dce(PR #7). - Files:
pages/api/auth-utils.js('your-secret-key')pages/api/auth/login.js,pages/api/auth/register.js,pages/api/auth/verify.jspages/api/favorites.js,pages/api/users/search.jslib/permission-middleware.js
- Impact: If
JWT_SECRETenv var is unset (e.g. preview/staging misconfig), tokens are signed with'your-secret-key-change-in-production'— an attacker can sign their own admin token in 5 seconds. - Fix: Centralize JWT_SECRET access in one helper that
throws at module load ifprocess.env.JWT_SECRETis unset. Every other file imports from there. - Bonus: Token expiry is inconsistent (
/api/auth/login.jsuses 24h,pages/api/auth-utils.jsuses 7d). Pick one. - As-shipped:
lib/auth-secret.jsis the single source of truth and throws at module load ifJWT_SECRETis unset. Canonical TTL isJWT_TOKEN_TTL = '24h'. All 7 literal fallback sites are converted to import-and-throw.test/lib/auth-secret.test.js(3 tests) covers the fail-loud path. - Owns:
role-architect+role-implementer.
3. Default admin credentials in seed + README — RESOLVED 2026-05-23
- Resolved by:
drop-public-setupBrief 1 (commitff80753) + Brief 2 (commitb63b509). PR #13. - Files:
scripts/setup-neon-db.jslines 130-138 — createsadmin@tcgvault.com/admin123README.mddocuments the credentialspages/api/setup-database.js— duplicates the setup AND is an UNAUTHENTICATED public POST endpoint withAccess-Control-Allow-Origin: *
- Impact: Anyone who hits
/api/setup-databasecan re-trigger DDL. Theadmin123password is one Google away from public knowledge. - Fix:
- Delete
pages/api/setup-database.js. Schema setup is a one-time job; it should not be a route. - Change
setup-neon-db.jsto require aADMIN_INITIAL_PASSWORDenv var (no default). - Strip the admin password from README — replace with "run
npm run setup-dband follow the prompt".
- Delete
- As-shipped:
pages/api/setup-database.jsalready deleted byfix-auth-bypassBrief 3 (commitfc0dd73); theforbidden-endpointsCI job blocks re-introduction.scripts/setup-neon-db.jsnow readsADMIN_INITIAL_PASSWORDfromprocess.env; if unset or empty, the script writes an actionable error (names the env var, points at.env.local, suggestsopenssl rand -base64 24, mentions CI-secret alternative, references README) and exits with code 1 before opening any DB connection. The bcrypt input is the env-var value, not the literaladmin123. The twoconsole.loglines that previously echoedAdmin User: admin@tcgvault.com+Admin Password: admin123are deleted (R3 — stdout-leak prevention into CI logs); replaced with a singleAdmin user ready (email: admin@tcgvault.com)line that does NOT echo the password.README.md's "Default Admin Account" section replaced with "First-time admin setup" copy that documents the env-var requirement, theopenssl rand -base64 24generation tip, the CI-secret alternative, and an operator-rotation note for envs that pre-date this convoy.- Bonus (Decision D, Brief 2):
scripts/setup-neon-db.jsconverted from CommonJS to ESM sonpm run setup-dbactually executes on Node 22.x. Thebump-next-jsconvoy added"type": "module"topackage.jsonfor ESLint v9 flat config; the seed script'srequire()calls were silently broken since that landed. Without Brief 2, Brief 1's env-var gate would have been theatrical (script throwsReferenceErrorbefore reaching the gate).
- Operator caveat (R1, Decision A — going-forward only): the seed is idempotent (
ON CONFLICT (email) DO NOTHING); re-runningnpm run setup-dbon an env that already has the admin row does NOT rotate the password. Any deployed env that ran setup before this convoy still has the weakadmin123hash in its DB — operators must rotate manually via the app's profile settings, or wait for the queuedrotate-default-adminfollow-up convoy. Documented inAGENTS.mdGotcha #4 and the README's First-time admin setup blockquote. - Sibling weak-cred references deferred:
scripts/reset-db.js,scripts/create-test-users.js, andTESTING_GUIDE.mdstill hardcodeadmin@tcgvault.com/admin123— out of scope here per the no-go-zones rule (historical scripts) and the convoy spec. Queued forpurge-weak-creds-from-helpersfollow-up (or fold intopick-a-namesince the email is also changing). - Owns:
role-implementer.
4. Dev-only test endpoints shipped to production — RESOLVED 2026-05-23
- Resolved by:
fix-auth-bypassBrief 3, commitfc0dd73(PR #6). - Files:
pages/api/simple.js,pages/api/test-auth.js,pages/api/test-db.js,pages/api/setup-database.js. - Impact: Unknown — depends on what they expose.
/api/test-dblikely returns the DB connection string;/api/test-authmay leak token-handling details. - Fix: Delete all four. Add a CI grep that fails the build if any file matching
pages/api/(test-|simple|setup-)*.jsexists. - As-shipped: All four files deleted.
.github/workflows/ci.ymlhas a newforbidden-endpointsjob (blocking) that fails the build if any of the four paths reappear OR if a newpages/api/test-*.jsfile is added. Local simulation in the implementer PR confirmed clean → OK, withtest-fake.js→ FAIL, post-cleanup → OK. - Owns:
role-implementer.
5. CORS Access-Control-Allow-Origin: * on auth endpoints — RESOLVED 2026-05-24
- Resolved by:
fix-auth-bypassBrief 4, commit297afca(PR #9, login + register) +cors-tighten, squash commitda50d78(PR #19, the remaining 24 handlers + CI regression-lock). - Files: at minimum
pages/api/auth/login.js,pages/api/auth/register.js,pages/api/setup-database.js(verify others). - Impact: Any origin can submit credentials. Combined with the no-rate-limit problem below, credential stuffing is wide open.
- Fix: Set
Access-Control-Allow-Originto the literal frontend origin (https://tcgvault.com/ preview domain), or remove the header entirely if the API and the frontend are same-origin (they are, on Vercel). - As-shipped (Brief 4, 2026-05-23):
pages/api/auth/login.jsandpages/api/auth/register.jsdropped the foursetHeadercalls + the OPTIONS preflight handler.pages/api/setup-database.jswas deleted entirely by Brief 3. - As-shipped (
cors-tighten, 2026-05-24, squash commitda50d78, PR #19, architect-commitec22b70, implementer-commita843736):- 24
pages/api/**handlers swept —admin/index.js,auth/verify.js,cards/[id]/ownership.js,cards/owned.js,cards/search.js,collections.js,collections/[identifier].js,collections/[identifier]/activity.js,collections/[identifier]/cards.js,collections/[identifier]/permissions.js,collections/[identifier]/thumbnails.js,community/collections.js,favorites.js,invite/accept.js,invite/decline.js,public/collections.js,user/avatar.js,user/avatar/generate.js,user/delete.js,user/password.js,user/profile.js,user/settings.js,user/stats.js,users/search.js. Each diff is a pure deletion of 9-11 lines (the leading// Set CORS headerscomment + 3setHeadercalls + the leading// Handle preflight requestscomment + the 4-line OPTIONS-if block + the trailing blank line). No additions per source file. Pattern split: 16 Pattern A (top-level method gate after the CORS block) + 8 Pattern B (method-branched inside thetryblock). Both shapes documented verbatim in.convoys/cors-tighten/brief-1-sweep-wildcard-cors.md. - New blocking
forbidden-cors-headersCI job in.github/workflows/ci.yml, modeled verbatim on the existingforbidden-endpointsjob (added byfix-auth-bypassBrief 3). Grepspages/api/forAccess-Control-Allow-(Origin|Methods|Headers), emits::error file= line=::annotations on hit, exits 1. Nocontinue-on-error, no|| truewrapper. Sits betweenforbidden-endpointsandtestin the YAML for logical grouping (bothforbidden-*checks are static-source guards before the runtime test job). Runs in ~4 seconds; zero new dependencies. - All five architect decisions self-ratified at gate 1 (no operator decisions needed) — D1 Option B (expanded sweep, all 24 files), D2 delete the OPTIONS preflight handler entirely (Option (a)), D3
verify.jsAllow-Methodstightening moot (subsumed by D2), D4 no new per-route handler tests in this convoy (deferred to queuedfill-vitest-handler-coverage), D5 add the new CI regression-lock job. - Diff: 25 files, +29 / -261 (pure deletion across 24 source files; 29 additions = the new CI job).
- 24
- As-shipped metrics (post-merge run 26378806555 + subsequent runs):
forbidden-cors-headers(new) — PASS in 4s. First live exercise of the regression-lock; greps clean against the post-sweep tree.Playwright smoke— PASS in 56s, 3/3 tests in 3.3s against the post-CORS-removal Vercel preview. Cross-validates that CORS removal is safe for the auth surface (smoke's sign-in check still passes against/login;/api/healthstill serves anonymously). Surfaced as a real CI signal even though Decision D4 deferred per-route handler tests — the existing smoke spec transitively defends the auth + public surfaces against this convoy's deletions.Screenshot diff— workflow exited 0 because ofcontinue-on-error: true, but the actual visual test failed with the documented "snapshot doesn't exist" error (Decision-4 end state ofadopt-playwright-smoke). Triggered on PR #19 despite this being API-only because itspaths:filter ispages/**which matchespages/api/**too — minor false-positive queued astighten-visual-diff-path-filter(see § Queued convoys).- All other gates (
Lint,Vitest,Schema map up to date,forbidden-endpoints) — green.
- Implementer subagent-retry footnote (transient). The implementer's PR report flagged that HEAD was already at the implementer commit (
a843736) when its retry subagent woke up — a prior implementer run had completed the work, and the retry's "STOP per branch mismatch" rule kicked in; the retry then ran verification only (lint baseline, vitest 21/21, grep clean, YAML valid) and reported success. This is a transient subagent retry, not a process gap. The implementer commita843736is canonical; the squashda50d78rolls up the architect plan + Brief 1 + the implementer's work without duplication. - Operator action required going forward: none. No env vars to seed, no secrets to rotate, no infra changes. The
forbidden-cors-headersjob is self-contained (plain bash grep on the runner); future PRs that accidentally re-scaffold a wildcard CORS header will fail the build with a file-and-line pointer to the offending line. - Owns:
role-implementer.
6. No rate limiting anywhere — RESOLVED 2026-05-24 (milestone — last P0 closed)
- Resolved by:
fix-auth-bypassBrief 4, commit297afca(PR #9, login + register only) +add-rate-limiting, squash commit708ef45(PR #20, the remaining surface + 3-import-route gating + 1 atomic admin UI fix + 1 rule extension). - Impact: Login endpoint accepts unlimited attempts; card-search endpoint can be hammered; image upload endpoints can be exhausted. The
pages/api/cards/import-*.jsendpoints externally hit Scryfall/Pokémon APIs with no caller throttling. - Fix: Adopt
@upstash/ratelimit(free tier covers a small launch) or Vercel's built-in middleware-based rate limiting. Apply to:/api/auth/login,/api/auth/register,/api/users/search,/api/cards/search, all/api/cards/import-*, and/api/user/avatar*(upload). - As-shipped (Brief 4, 2026-05-23):
lib/rate-limit.js(new) providescheckAuthRateLimit(req)via@upstash/ratelimit@^2.0.8+@upstash/redis@^1.38.0(5 attempts / 15-min sliding window per IP). Wired into login + register. Env vars areKV_REST_API_URL/KV_REST_API_TOKEN(auto-provisioned by Vercel's Upstash Marketplace integration — note this is a rename from the brief's originalUPSTASH_REDIS_REST_*spec; see.convoys/fix-auth-bypass/brief-4-tighten-auth-surface.md§ Post-merge addendum). Fails closed in prod when env vars are unset; warn-and-no-ops in dev. Search / import / avatar endpoints were unchanged at that point (the deferred surface thatadd-rate-limitingthen closed). - As-shipped (
add-rate-limiting, 2026-05-24, squash commit708ef45, PR #20, architect-commit60b842e, implementer-commit51a3a97):-
lib/rate-limit.jsrefactored from a single auth-onlyRatelimitinstance into aMap<className, Ratelimit>cache with one shared Redis client and 5Ratelimitinstances (one per class, distinct Redis prefix). Five exported functions:checkAuthRateLimit(req)(Brief 4 contract preserved byte-identical),checkSearchRateLimit(req),checkUploadRateLimit(req, userId),checkGenerateRateLimit(req, userId),checkImportRateLimit(req, userId). Internalcheck(className, identifier)shared helper.LIMITER_CONFIGis a top-levelconstmap of{ limit, window, prefix }per class; adding a sixth class is a one-line addition + one new exported function. -
6 routes newly gated with the appropriate per-class limiter at the correct ordering (auth before user-keyed limiter; method check first):
Limiter Limit/window Key Routes checkAuthRateLimit(unchanged from Brief 4)5 / 15 min IP auth/login.js,auth/register.jscheckSearchRateLimit(new)60 / 1 min IP users/search.js,cards/search.jscheckUploadRateLimit(new)10 / 1 hour user user/avatar.jscheckGenerateRateLimit(new)5 / 1 hour user user/avatar/generate.jscheckImportRateLimit(new)5 / 1 hour user (admin-only) cards/import-mtg.js,cards/import-pokemon.js,cards/import-lorcana.js -
extractUserIdentifier(userId)THROWS onnull/undefined/''/NaN(Decision 4 defensive shape). Surfaces gate-ordering bugs at dev time rather than silently falling back to IP and converting a per-user limit into a per-IP limit — which would lock other household members out for one user's behavior. Numeric0is intentionally accepted (returns'user:0') for forward-compat. -
Three
pages/api/cards/import-*.jsroutes newly auth-gated. Each grewgetUserFromRequest+if (user.role !== 'admin') return 403+checkImportRateLimit(req, user.userId)before the existingtryblock. Closes the publicly-callable anonymous-abuse vector the architect's pre-brief audit flagged (each handler hits Scryfall / Pokémon-TCG / Lorcana APIs and performs UPSERTs intocardswith no caller throttling pre-convoy). Lorcana was gated defensively despite zero current frontend callers — route removed in PR #59 (delete-dead-lorcana-import, RESOLVED 2026-06-02). -
Atomic admin UI fix in
pages/admin/card-import.js. Added'Authorization': \Bearer ${localStorage.getItem('auth_token')}`` to the import fetch's headers (one-line addition). This was the architect's critical pre-brief discovery and the reason Decision 1 routed back to the operator — gating the import APIs without this matching client fetch fix would have closed P0 #6 but introduced an immediate 401 on every "Import Cards" click, producing a visible UX regression on the only live admin tooling that exercises the gated routes. Shipping the API gate + the client fix in the same atomic PR is what made Decision 1 Option A viable. -
.cursor/rules/api-routes.mdc§ Rate limiting extended with the per-class table + verbatim call shape + gate-ordering rules (method check first; auth before any user-keyed limiter; admin-role check goes between auth and rate-limit for the import routes) + identifier-extraction documentation + uniform 429 response shape + fail-closed env-var contract + fail-open Upstash-outage behavior. Doc-writer pass verified the implementer's extension is complete; no further touch-ups needed. -
All six architect decisions ratified at gate 1. D1 operator-ratified (Option A — gate all three import routes plus the atomic admin UI fix); D2-D6 architect-self-ratified per the precedent established by
cors-tightenD2-D5 andfix-vercel-deployment-protection-in-ciA/B/D (hybrid named-limiter shape; per-class limit values with tuning evidence; two-extractor shape with defensive THROW; uniform 429 message; no new vitest / playwright specs in this convoy). -
Diff: 12 files, +1612 / -23 in the squash. The 1612-addition figure is dominated by the architect's convoy + brief files (676 + 751 lines) which the squash includes because the architect commit preceded the implementer commit on the same branch. Actual source-file diff is much smaller:
lib/rate-limit.js+90/-23 (lib refactor);.cursor/rules/api-routes.mdc+41 (rule extension); 6 route files +69 total (3× +16 for import routes, 4× +7 for search/avatar/generate/users-search);pages/admin/card-import.js+1 (Bearer-header addition).
-
- As-shipped metrics (post-merge run 26382185019 + subsequent runs):
Playwright smoke— PASS in 59s, 3/3 tests in 3.8s against the post-rate-limit Vercel preview (home redirects ✓ 431ms / sign-in page renders ✓ 331ms //api/health✓ 193ms). Critical cross-validation: smoke calls/api/healthonce per run (well below the new search class's 60/min ceiling), and the home + sign-in routes don't touch any of the 6 newly-gated endpoints — so the new search limiter does NOT 429 the smoke spec. The cross-validation lineage now accumulates across three convoys: smoke test 2 still passes against post-PR-#15 Layout default-user + post-PR-#19 CORS-tighten + post-PR-#20 rate-limiting — the same 3-test spec has defended the auth surface through three sweeping changes without anyone writing a dedicated test.forbidden-cors-headers(fromcors-tighten) — PASS. The convoy is purely additive of rate-limit gate code; no CORS headers were reintroduced.forbidden-endpoints(fromfix-auth-bypassBrief 3) — PASS. No newpages/api/test-*.jsor deleted-endpoint shapes reintroduced.Unit tests (vitest)— PASS, 21/21 in 27s. Decision 6 (no new vitest specs) verified at architect time (rg 'rate-limit|@upstash' test/returns zero matches; the existing 21 specs don't transitively importlib/rate-limit.js, so the lib refactor was strictly safer than the convoy file's stale § Known constraints implied).Lint— 128 problems (baseline preserved, no regression). Zero new lint problems from the lib refactor, the 6 route edits, or the admin UI one-liner.Screenshot diff—continue-on-error: trueswallow peradopt-playwright-smokeDecision 4 (no baseline committed yet). Triggered on PR #20 because thepaths:filterpages/**matches the 6 route edits underpages/api/; same minor false-positive as PR #19, tracked by the queuedtighten-visual-diff-path-filterfollow-up. Not a regression.- All other gates (
Schema map up to date,Aggregate gate) — green.
- Operator action required going forward: none. All Upstash env vars (
KV_REST_API_URL/KV_REST_API_TOKEN) were already auto-provisioned via the Vercel Marketplace integration for Brief 4. No new secrets, no infra changes, no CI gates to enable. The fail-loud-in-prod predicate inlib/rate-limit.js::init()is self-defending: if a future deploy unsets either env var, every gated route fails closed on the first call (throw new Error('[rate-limit] Upstash not configured...')). If a follow-up tuning need surfaces (search 60/min too tight, generate 5/hour too tight), the fix is a single-lineLIMITER_CONFIGedit; surface astune-search-rate-limitortiered-rate-limitsonly if real users 429. - Owns:
role-architect(pattern + Decision 1 routing) →role-implementer(per-route).
7. Layout default-prop leaks maintainer email — RESOLVED 2026-05-24
- Resolved by:
fix-layout-default-userconvoy (PR #15, squash commitca302a8). Brief 1 (pre-squashddf8fd2) shipped the Layout default-null + logged-out branch + vitest lock-in; Brief 2 (pre-squash8c7d127, rebased to0f6bfbbpre-merge) swept the 7 pages that needed page-level fixes. - File:
components/Layout.jsline 562:function Layout({ children, user = { email: 'me@randallstillwell.com', role: 'user' }, ... }). - Impact: Any page that renders Layout without passing a
userprop displays your real email and impersonates you as the logged-in user. - Fix: Default
user = nulland render a logged-out state branch. Verify every page passesuserexplicitly (the graph shows ~13 pages callLayout; audit each). - As-shipped:
components/Layout.jsdefault prop changed from hardcoded{ email: 'me@randallstillwell.com', role: 'user' }tonull.UserProfileDropdownnow branches onuser === nulland renders a<Link href="/login">Sign in</Link>CTA in place of the avatar + email + dropdown menu (NavigationContent'sauthenticatedNavigation/myCollectionNavigation/adminNavigationwere already null-safe via existing optional chains; no change there).- 7 pages swept (Brief 2, 11
<Layout>call sites total).pages/scanner.js(×1),pages/decks.js(×3),pages/deck-builder.js(×4),pages/deck/[id].js(×3) now passuser={user}explicitly.pages/profile.jsandpages/settings.jsreplaced their leakyuseState({ email: 'me@randallstillwell.com', role: 'admin' })initializer withuseState(null)(15 syncuser.*reads in profile + 1 in settings got null-guards).pages/card/[id].jsreplaced its hardcodedconst user = { email: 'me@…', role: 'user' }withconst { user } = useAuth()fromlib/use-auth.js. - 10 pages already correct (architect's per-page audit, Decision B in
.convoys/fix-layout-default-user.md):dashboard,my-cards,cards,collections,collection/[identifier],community/collections,admin/card-import,admin/card-editor,invite/accept,invite/decline. No changes there. - Test coverage:
test/components/Layout.test.js(new) adds 5 regression-lock assertions — no maintainer email whenuserisnull/omitted; "Sign in" link present when logged out; supplied email renders when supplied; no accidentalGuestplaceholder. Vitest 21/21 green at merge (16 pre-existing auth tests still green). - New devDeps:
jsdom@^29+@testing-library/react@^16(test-only).vitest.config.jsgot a 3-lineesbuildblock to parse JSX in.jsfiles (per-file// @vitest-environment jsdomdirective — no global env change). - Verification at merge:
rg 'me@randallstillwell.com' pages/→ 0 hits; anonymouscurl /cardsreturned HTTP 200 with no maintainer email; lint baseline match (128 problems, unchanged); CI Aggregate gate / Lint / Vitest / Vercel preview / forbidden-endpoints all green.Playwright smoke+Screenshot diffred but for an unrelated CI-infra reason — see CI infrastructure side-effect note below.
- Flagged-but-deferred (deliberately out of scope per the convoy spec):
- 4 pages still import
useAuthfromlib/auth-context.js(pages/scanner.js,pages/decks.js,pages/deck-builder.js,pages/deck/[id].js) — collapsing the three parallel client-side auth surfaces is the queuedsingle-auth-providerconvoy (P1 #9 in this file), not this one. components/MobileNavigation.jsstill receives a deaduserprop (it accepts{ user, onMenuOpen }but never readsuser.*— the bottom-bar items are static). Queued ascleanup-mobile-nav-dead-props(or fold intogod-component-splitif that lands first).pages/card/[id].jsstill importsuseIsAdminfromlib/admin-auth.js— third parallel auth surface; samesingle-auth-providerconvoy will collapse it.
- 4 pages still import
- CI infrastructure side-effect (not part of this convoy). PR #16 (squash commit
7e97254) landed alongside as a CI permissions fix, adding scopedpermissions:blocks to.github/workflows/preview-smoke.yml+.github/workflows/visual-diff.yml. That fixed the 5-second 403 "Resource not accessible by integration" failure on both workflows but exposed a second issue: with permissions correct, both now reach the actual deployment check and 10-min-timeout against Vercel Deployment Protection's 401 SSO challenge (anonymous GitHub runner GETs the preview URL). New queued convoyfix-vercel-deployment-protection-in-ci(.convoys/fix-vercel-deployment-protection-in-ci.md) tracks that follow-up. - Owns:
role-implementer.
8. Next.js 15.4.3 — Vercel platform blocks deploys (vulnerable version) — RESOLVED 2026-05-23
- Resolved by:
bump-next-jsconvoy, single-brief PR commite57ea17("bump: next 15.4.3 -> 16.2.6, ESLint flat config (v9 fallback), typescript devDep"). The Vercel platform gate cleared with the first successful deploy on the same date; every subsequent PR (fix-auth-bypass,drop-public-setup,fix-layout-default-user, the CI permissions fix) has had a green Vercel preview. - Discovered: 2026-05-22 during the bootstrap PR CI run. Vercel build completes successfully (~29s) but the deployment exits with status
Errorand"Vulnerable version of Next.js detected, please update immediately". - Files:
package.jsonline 22 ("next": "^15.4.2"→ locked at15.4.3),package-lock.json. - Impact: Vercel will not deploy any branch — including
main— until Next.js is bumped. Preview URLs are unavailable, which meanspreview-smoke.ymlandvisual-diff.ymlcan't fire. The last successful deploy onmainwas 2025-08-01; production may already be running an outdated build. - CVE context: Next.js shipped a middleware auth-bypass advisory (CVE-2025-29927) patched in 15.2.3, plus subsequent advisories. The exact CVE Vercel is flagging on 15.4.3 needs confirmation via
npm auditand the Next.js security advisory page. - Fix: Bump
nextto the latest secure 15.x (npm install next@^15.5and run smoke tests) OR the latest 16.x (next@^16.2.6— major bump; review breaking changes in Next.js 16 release notes). - As-shipped (Decision A in
.convoys/bump-next-js.md— leapfrog to 16):next:^15.4.2→^16.2.6(resolves to16.2.6).eslint-config-next:15.4.2→^16.2.6. Config migrated from.eslintrc.jsontoeslint.config.mjs(eslint-config-next@16 is flat-config-only).eslint:^8→^9.39.4(Decision D fallback — v10 surfaced Risk R15 empirically because@typescript-eslint/scope-manager@8.59.4bundled byeslint-config-next@16doesn't implement v10's newaddGlobalsAPI; v10 adoption deferred to a separatebump-eslint-10convoy, upstream-blocked on typescript-eslint).typescript: newly added at^5.9.3as a devDep (Decision C — required by the typescript-eslint chain regardless of ESLint major; no project source migration to TS).scripts.lint:"next lint"→"eslint ."(next lint removed in 16). Lint baseline grew from ~100 to 128 problems (81 errors, 47 warnings) due toeslint-plugin-react-hooks@7.1.1+@next/eslint-plugin-next@16.2.6rule additions; CI tolerates this via the|| truewrapper in.github/workflows/ci.ymlper P1 #11.5 (fix-lint-baseline).next.config.js:images.domains→images.remotePatterns(deprecated and removed in 16; preserves Scryfall, Pokémon TCG, Lorcana API hosts for eventualnext/imageadoption).- Verification at merge:
npm installclean (no ERESOLVE),npm run buildexit 0 with Turbopack (~1.4s compile, 23 static pages + 47 API routes), first green Vercel deploy onmainsince 2025-08-01.
- Side-effects (deliberately deferred, not part of this convoy):
bump-react(React 18 → 19) — held until 18.x EOL or until a feature needs it.- App Router migration — multi-month effort; queued indefinitely.
adopt-vitest✅ shipped asfix-auth-bypassBrief 5;adopt-playwright-smokepartially shipped via the Vercel-bound workflows (CI infra now blocked byfix-vercel-deployment-protection-in-ci).fix-lint-baseline(P1 #11.5) — RESOLVED 2026-06-02 (PRs #61–#63); lint baseline 0; CI lint blocking.bump-eslint-10+bump-typescript-6— upstream-blocked on typescript-eslint shipping v10-tested releases.
- Doc drift note: this resolution was applied as part of the
fix-layout-default-userpost-convoy cleanup (commit reflectingb7ddd08's sibling) — thebump-next-jsconvoy never ran a dedicated doc-writer pass, so this RESOLVED entry was added ~24h after the fix actually shipped. - Owns:
role-architect(pick target version + assess breaking changes) →role-implementer(bump + verify dev/build/start + smoke). - Convoy:
bump-next-js— ran beforefix-auth-bypass. Without this convoy, every L3 gate that depends on a Vercel preview was non-functional.
P1 — pre-launch quality bar
8. Two SQL clients in parallel (@neondatabase/serverless + @vercel/postgres) — RESOLVED 2026-05-26
- Resolved by:
single-sql-clientconvoy, squash commitc403ea4(PR #30). Parent-owned end-to-end (no architect or implementer subagent dispatched per the convoy file's "Owns" decision; single-file proven-pattern surface, mirroring thefix-reset-db-scriptprecedent). - Impact (pre-fix): Two different param-handling APIs, two different transaction stories, two different connection-pool stories. Plus
lib/database.js's manual interpolation +sql.unsafe(query)was a SQL-injection vector if any caller passed user input through. Architect audit found no current call site actually exercised the unsafe shape with user input (the 2 callers pass a numericusers.idfrom a verified JWT), so this was foot-gun removal rather than a live security finding — see.convoys/single-sql-client.md§ D4 for the audit. - As-shipped:
lib/database.js(47 lines) deleted. TheDatabaseAdapterabstraction is gone; no replacement.- 2 callers migrated (the convoy spec's "~3 files based on graph" estimate was loose; architect grep confirmed exactly 2):
pages/api/auth-utils.js—isAdmin(userId)+getUserById(userId)swapped fromdb.query(\SELECT … WHERE id = $1`, [userId])to `` await sqlSELECT … WHERE id = ${userId}`` (byte-equivalent SQL, identicalresult.rows[0]access pattern). Import swapped from'../../lib/database.js'to'@vercel/postgres'`.test/api/auth-utils.test.js— dropped the now-unusedvi.mock('../../lib/database.js', ...)call and the unusedviimport. The 5 tests (2generateToken+ 3verifyToken) are unchanged; they never exercisedisAdmin/getUserByIdin the first place.
@neondatabase/serverlessretained as a runtime dep. 11scripts/*helpers still importneon()directly (setup-neon-db.jsadmin seed,migrations/2026-05-24-rename-admin-email.js,reset-db.js, plus 8 historicaladd-*.js/fix-*.js/seed-*.jsjobs) — all out of scope per the no-go-zones rule. The dep-purge is tracked as the newly queuedpurge-neondatabase-serverless-fullyfollow-up (unblocked by PR #32migration-tool— the migration helpers now go throughnode-pg-migrate'spgclient, not@neondatabase/serverless).
- As-shipped metrics (PR #30, merged 2026-05-27T03:54:01Z UTC / local 2026-05-26):
- Diff: 4 files, +447 / -64. The 447-addition figure is dominated by
.convoys/single-sql-client.md(the planning document, committed atomically). Actual source-file diff is small:pages/api/auth-utils.js+5 / -7,test/api/auth-utils.test.js+1 / -5,lib/database.js0 / -47. Lint— 125 problems (baseline preserved post-single-auth-provider; thelib/database.jsdeletion did not change lint count because the file was already lint-clean).Vitest— 21/21 pass.Playwright smoke— 3/3 pass (the smoke spec doesn't exerciseisAdmin/getUserById, but the deployed preview is unaffected by the swap, so the cross-validation lineage continues).Screenshot diff— not triggered (PR #30's diff ispages/api/**+lib/**+test/**+.convoys/**; the post-PR-#26!pages/api/**exclusion correctly held — see PR #26 below).forbidden-endpoints+forbidden-cors-headers— green.
- Diff: 4 files, +447 / -64. The 447-addition figure is dominated by
- Operator action required going forward: none. No env-var change; no schema change; no infra change.
- Owns:
role-architect(audit + decisions D1-D5 in.convoys/single-sql-client.md) — same parent agent that implemented.
9. Three parallel client-side auth implementations — RESOLVED 2026-05-26
- Resolved by:
single-auth-providerconvoy, squash commit0668b0c(PR #31). Parent-owned end-to-end (architect + implementer rolled together — mechanical diff once D1's shape-parity decision was made). - Files (pre-fix):
lib/auth-context.js(AuthProvider/useAuth),lib/admin-auth.js(AdminProvider/useAdmin/useIsAdmin),lib/use-auth.js(useAuth). - Impact (pre-fix): Pages randomly imported from one of three places. State was duplicated. Logout in one provider didn't necessarily clear the others. Token-verify roundtrips happened up to 3× on
pages/card/[id].jsmount. - As-shipped:
lib/auth-context.js+lib/admin-auth.jsdeleted. No replacement;lib/use-auth.js's hook-onlyuseAuth()is the sole client auth surface.- Importer inventory was 7 source files, not the ~30 estimated in P1 #9. The estimate was pre-
fix-layout-default-user(PR #15,ca302a8); that convoy had already migrated most of the tree tolib/use-auth.js, so the residual surface was much smaller than the estimate. Architect grep (rg "from ['\"].*lib/auth-context['\"]" --type js) returned exactly 6 importers ofauth-context.js(pages/_app.js,pages/index.js,pages/scanner.js,pages/decks.js,pages/deck/[id].js,pages/deck-builder.js) + 1 importer ofadmin-auth.js(pages/card/[id].js). <AuthProvider>wrapper removed frompages/_app.js. Per D3:useAuth()fromlib/use-auth.jsis hook-only, no Provider needed.<ThemeProvider>stays.<AdminProvider>was never in the tree to begin with (confirmed by reading_app.jspre-convoy).useIsAdmin()'s lone consumer inlined.pages/card/[id].jswas the only consumer; replaced withconst isAdmin = user?.role === 'admin'from the existinguseAuth()call (D2). Rendering condition at line 524 unchanged byte-for-byte;adminLoadingkept as a local alias ofauthLoadingto keep the diff minimal.- Verify roundtrip count reduced 3 → 1 on
pages/card/[id].jsmount, and 2 → 1 on every other page-load. Single source of truth foruserstate per hook call site. - CODEOWNERS sweep:
.github/CODEOWNERSlines for the two deleted files removed (per.convoys/single-auth-provider.md§ Adjacent doc / config edits). - Doc surface updated atomically:
AGENTS.md§ 2 + § 3,.cursor/rules/auth-and-permissions.mdc,.cursor/rules/no-go-zones.mdc,.cursor/skills/add-page/SKILL.mdall swept to describe the post-convoy single-surface state. (This entry's parent post-convoy doc-writer pass keeps that work consistent acrossship-readiness.mdand AGENTS.md.)
- As-shipped metrics (PR #31, merged 2026-05-27T03:58:08Z UTC / local 2026-05-26):
- Diff: 15 files, +341 / -263. 2 file deletions (
lib/auth-context.js,lib/admin-auth.js); 13 modifications (7 source pages +.github/CODEOWNERS+ 5 docs / rules / skills + the new.convoys/single-auth-provider.mdplanning doc). Lint— 128 → 125 problems (3 fewer errors; the deleted files contained 3 unused-import / unused-var lints; no new lint surface introduced). This is the new lint baseline.Vitest— 21/21 pass. The 4 test files don't import any of the deleted modules (grep-confirmed pre-convoy).npm run build— succeeds end-to-end; 26 pages compile (10 dynamic API routes + 16pages/**views including every file modified by the sweep). No "useAuth must be used within an AuthProvider" runtime error during SSR — confirms<AuthProvider>removal is safe.Playwright smoke— 3/3 pass.Screenshot diff— triggered (PR #31 touchespages/**non-API pluscomponents/**adjacent surface),continue-on-error: trueswallow per Decision-4 end state ofadopt-playwright-smoke(no baseline committed yet).
- Diff: 15 files, +341 / -263. 2 file deletions (
- Operator action required going forward: none. No new env vars; no schema change.
- Spec deviation: none of substance. Pre-merge estimate of ~30 importers in P1 #9 was loose; actual was 7 (documented above as the as-shipped reality).
- Owns: parent (architect + implementer rolled together per the convoy file's "Convoy owner" line).
10. No tests
- Impact: The first agent-driven refactor of
getUserFromRequest(P0 #1) is high-blast-radius with no safety net. - Fix sequence:
- Install
vitest. Addnpm run test:runscript. RESOLVED byfix-auth-bypassBrief 5, commit1629afb. - Install
@playwright/test. Wire uptests/smoke/app.smoke.spec.ts(already drafted; needsplaywright.config.ts). RESOLVED 2026-05-24 byadopt-playwright-smoke, PR #18 squash7b6f751— 3/3 smoke tests pass in 2.9s, full workflow 59s, zero secret leaks. See § Queued convoys and.convoys/adopt-playwright-smoke.md§ As-shipped. - Re-enable the
test:job in.github/workflows/ci.yml(commented out at install time). Next remaining step in this fix sequence. - Add unit tests for
lib/permission-middleware.js,lib/slug-utils.js,pages/api/auth-utils.js. - Wire
preview-smoke.ymlto run against the Vercel preview URL. RESOLVED 2026-05-24 byfix-vercel-deployment-protection-in-ci(PR #17,9a3e077) +adopt-playwright-smoke(PR #18,7b6f751).
- Install
- Owns:
role-architect(test strategy) →role-implementer(initial suite).
11. No migration tool — scripts/add-*.js graveyard — RESOLVED 2026-05-26
- Resolved by:
migration-toolconvoy, squash commitde9f334(PR #32). Parent-owned end-to-end (no architect or implementer subagent dispatched; the convoy spec pre-ratified each Decision's recommended path, and the implementation surface was a small set of well-bounded file edits — see.convoys/migration-tool.md§ Subagent / multitask footnote). - Files (pre-fix): 27+ scripts in
scripts/of the formadd-foo-column.js,fix-bar-constraint.js,seed-baz.js. No idempotency tracking, noschema_migrationstable, no rollback. - Impact (pre-fix): Onboarding a new env required re-running every script in the right order. No way to know what had been run on a given Neon branch. Every new column was at risk of being missed in prod.
- As-shipped (7 architect decisions, all ratified verbatim from the convoy spec at gate 1):
- Tool:
node-pg-migrate@^8.0.4(D1). JavaScript-native, raw-SQL-friendly viapgm.sql(), ESM-clean. Rejecteddrizzle-kit/prisma migrate/kyselybecause each would force broader TypeScript surface thanAGENTS.mdGotcha #9 allows. Bringspg@^8.21.0as a peer dep (dev-only). - Migrations directory:
migrations/at the repo root (D2). Separates the new tool-wrapped artifacts from the legacyscripts/migrations/placeholder (which still houses2026-05-24-rename-admin-email.jsand is preserved per no-go-zones). Matchesnode-pg-migrate's default--migrations-dir migrations. - Tracking table: default
pgmigrations(D3) — no name collision in the existing schema. - Initial backfill:
migrations/1779853647564_initial-schema.js(~155 lines). Sevenpgm.sql(\CREATE TABLE IF NOT EXISTS ...`)blocks reproducingscripts/setup-neon-db.js's 7-table DDL verbatim (users / cards / user_cards / collections / collection_cards / decks / deck_cards). Idempotent against fresh AND pre-existing envs (theCREATE TABLE IF NOT EXISTSshape is a no-op on existing tables; only thepgmigrations` row changes). setup-neon-db.jssplit (D5): now (1) validatesADMIN_INITIAL_PASSWORD+POSTGRES_URL, (2) spawnsnpm run migrate upvianode:child_process.spawnwithstdio: 'inherit'and rejects with a wrapped error on non-zero exit, (3) seeds the admin-row INSERT withON CONFLICT (email) DO NOTHING. The sevenCREATE TABLE IF NOT EXISTSblocks are removed fromsetup-neon-db.js— they live in the migration now.- CI integration: deferred (D6) to the newly queued
wire-migrate-into-cifollow-up convoy. Real work (test DB + secret OR Postgres service container) not in scope; documented as known limitation in.convoys/migration-tool.md§ R3. - Down-migration on the initial backfill: hard stub that throws (D7). Rolling back would drop every user / card / collection / deck row. The stub's error message names the recommended alternative (Neon branch + forward-apply). Future migrations should write their own real
down(). - Doc surface updated atomically:
README.md(§ Installation + new § "Schema changes (post-migration-toolconvoy)"),AGENTS.md§ 3 (new "Schema changes" bullet) + § 4 Gotcha #6 (flipped → RESOLVED),.cursor/rules/no-go-zones.mdc(rewritten "Schema changes" rule),.cursor/rules/db-and-schema.mdc(§ "Schema source of truth" rewritten),docs/SCHEMA_MAP.md(preamble re-scoped). The 27+ historicalscripts/add-*.js/fix-*.js/seed-*.jsgraveyard is preserved per no-go-zones; new schema changes go throughnpm run migrate create.
- Tool:
- As-shipped metrics (PR #32, merged 2026-05-27T04:01:59Z UTC / local 2026-05-26):
- Diff: 10 files, +1230 / -136. The 1230-addition figure includes
.convoys/migration-tool.md(~600 lines),migrations/1779853647564_initial-schema.js(~155 lines), the doc edits, andpackage-lock.jsonchurn for thenode-pg-migrate+pginstall. Lint— 125 problems (baseline preserved post-single-auth-provider; the new migration file is lint-clean, no new ignore patterns ineslint.config.mjs).Vitest— 21/21 pass in ~1.3s. Vitest doesn't touch the migration surface; the run stayed green.node --check migrations/1779853647564_initial-schema.js→ exit 0.node --check scripts/setup-neon-db.js→ exit 0.- Module-load +
down()throw verification:node -e "import('./migrations/1779853647564_initial-schema.js').then(m => m.down())"throws the documented[migration:1779853647564_initial-schema] Refusing to drop the initial schema. ...message. npm run migrate -- --help→ returns standard node-pg-migrate help text through the wrapper.Playwright smoke— 3/3 pass (sixth consecutive convoy where the same 3-test smoke spec defends the auth surface through a sweeping change — see § Cross-validation in.convoys/migration-tool.md).
- Diff: 10 files, +1230 / -136. The 1230-addition figure includes
- Operator action required going forward: none for the convoy itself. The migration is idempotent against existing prod schema. No new env vars beyond the already-required
POSTGRES_URL+ADMIN_INITIAL_PASSWORD. Optional but recommended: the next deploy that runssetup-neon-db.jssilently applies the backfill migration (recording it inpgmigrations) — no operator action; this is just-in-time chained. - Live verification status: deferred per convoy spec — the parent did not have a throwaway Neon branch available. Optional post-merge sequence documented in
.convoys/migration-tool.md§ Operator runbook. - Spec deviation: none. All seven decisions landed verbatim from the spec at gate 1.
- Owns: parent (architect + implementer rolled together per the convoy file's § Subagent / multitask footnote).
11.5. Codebase has ~100 pre-existing ESLint errors — RESOLVED 2026-06-02
- Resolved by:
fix-lint-baselineconvoy, PRs #61 (309cfa2), #62 (c32bbd1), #63 (81bed51) — three file-group sweeps (components, pages, lib/config). Post-bump-next-jsbaseline had peaked at 128 problems (81 errors, 47 warnings); last pre-fix count was 125 aftersingle-auth-provider. - As-shipped:
npm run lintexits 0 with no problems;.github/workflows/ci.ymllintjob runsnpm run lint --if-presentwith no|| truecushion and nocontinue-on-error— lint failures block merge. - Discovered (historical): 2026-05-22 during the bootstrap PR. The repo had
"lint": "next lint"inpackage.jsonbut no.eslintrc.json— meaning lint was never run. Bootstrap added the config; lint surfaced ~100+ errors afterbump-next-js(ESLint 9 flat config + stricter react-hooks rules). - Convoy:
fix-lint-baseline— multitask fan-out by file group (components → pages → lib/config). - Owns:
role-architect(group strategy) →role-implementer(per-group fan-out).
12. Branding mismatch — "TCG Vault" vs. "Deck Hearth"
- Files: README,
package.json, seed data say "TCG Vault" /admin@tcgvault.com.components/Layout.jslines 596 + 689 render "Deck Hearth" + "DH" logo. The.env.localtemplate,vercel.json, and Vercel project name should also be audited. - Impact: Confusing for users. Confusing for marketing. Confusing for analytics. Pick one.
- Fix: Brand workshop → final name → global replace → update README, package.json
"name", every UI string, Vercel project name, email sender, support pages. Schedule a redirect from the old domain. - Owns:
role-ia-architect(which name? — needs human decision) →role-implementer.
P2 — refactor priorities
13. God components (10 files over 500 lines)
| File | Lines | Notes |
|---|---|---|
pages/cards.js |
1499 | AuthenticatedCards (886) + Card3D (502) live in one file. Split into pages/cards/index.js + components/Card3D.js. |
pages/collection/[identifier].js |
1044 | CollectionView is one mega-component. Extract: header, card-grid, share-modal-wrapper, edit-form. |
pages/collections.js |
989 | Similar structure to collection/[identifier]. Possibly share extracted pieces. |
pages/card/[id].js |
913 | CardDetail — split into header, owned-badge, add-to-collection-flow. |
pages/deck-builder.js |
823 | DeckBuilder — extract card-search, deck-list, mana-curve panels. |
components/CameraScanner.js |
~45 | RESOLVED 2026-06-02 — god-component-split slice shipped PRs #67–#72 + view extract. Pre-split ~1,050 lines; now composes useCameraScanner + useScannerIdentification + CameraScannerView. Logic lives in lib/scanner-card-detection.js, lib/scanner-card-identify.js, lib/scan-capture-upload.js, components/ScanDisambiguationDialog.js. |
pages/admin/card-editor.js |
778 | Form heavy. Use a useFormState pattern + separate the search-results subview. |
pages/scanner.js |
~75 | RESOLVED 2026-06-02 — god-component-split slice (Briefs 1–3): session/route libs (#74), useScannerQueue (#75), ScannerPageView. Pre-split ~825 lines. |
pages/settings.js |
669 | One screen per settings section is the usual fix. |
pages/profile.js |
625 | Avatar generation logic alone is ~150 lines — extract useGeneratedAvatar hook. |
Each is one convoy of its own. Use the architect role's slice_dependencies: to fan out implementers safely.
14. Schema-design smells (documented in docs/SCHEMA_MAP.md)
usershas two avatar columns (profile_image_url+avatar_url). Reconcile.collectionshas two visibility flags (is_public BOOLEAN+visibility VARCHAR). Reconcile.cards.quantity+cards.favoritedare unused (they belong onuser_cards/user_favorites). Drop.user_settingstable duplicates severaluserscolumns. Reconcile.- All enum-shaped VARCHARs (
role,condition,theme,game,visibility) should be CHECK-constrained or proper Postgres ENUMs. collections.tagsisTEXT(comma-separated). Migrate toJSONBor a join table.
15. Component coupling warning from graph
user-code-review-graph flagged:
- High coupling (44 edges) between
components-handleandpages-handle(largelyLayout,CardItem,ManaCost— expected for a shared UI surface). - High coupling (34 edges) between
lib-adminandapi-handler— almost all viagetUserFromRequest. After P0 #1 is fixed, this number stays high because the auth check is genuinely shared — that's fine.
16. Lots of inline SVG and emoji
The getIcon registry in Layout.js and MobileNavigation.js redefines the same SVG paths. Extract to components/icons/ with named exports. Then audit the codebase for inline SVG that should be a named import. Bonus: lazy-load the larger icon families.
P3 — UX, IA, design-system
Role-ia-architect findings
- URL structure — solid.
/cards,/collections,/collection/[slug],/deck-builder,/community/collections. Coherent. One quirk:/card/[id](singular) for detail vs./cards(plural) for index — typical Next.js shape but worth a redirect rule so/cards/[id]also resolves. - Logged-out homepage — current
pages/index.jsis 316 lines; needs an editorial pass. What's the value prop in one sentence? Right now it's mostly "we have cards". - Onboarding — signup → profile setup → first collection → scan-or-import card. Currently each step is a separate page. Consider a multi-step wizard at
/onboardingto keep the new user in flow. - Discoverability —
/community/decksand/community/forumsare in the nav but flagged as placeholders. Either ship the MVP for each before launch (forums likely too big) or hide the nav items until they exist.
Role-ux-reviewer findings
- Loading states — most data fetches set
loading: truethen re-render; very few show skeletons. Card grids should use shimmer placeholders; modals should disable submit while in flight. - Error states — error messages bubble to
console.errorand toast nothing. Add a global toast system (e.g.sonner) and wire every catch block. - Empty states —
/my-cardsand/collectionswhen empty drop to "no cards yet". Replace with first-time CTA: "Scan your first card" or "Browse popular sets". - Mobile drawer —
MobileNavigationis solid (recent commit442e906). One thing: the bottom-bar's active state contrast looks low in light mode; verify against AA. - Camera scanner UX — detection/identify logic split complete (PRs #67–#72); view markup in
CameraScannerView.js. Remaining polish: theme-token cleanup for overlay hex colors, busier toolbar simplification.
Role-design-system-auditor findings
- Two visual languages mixing — Tailwind classes AND CSS variables on the same elements. This is documented in
.cursor/rules/ui-and-theming.mdc; the cleanup is to define which property goes where and enforce. - Hardcoded hex colors — grep for
bg-\[#andstyle={{ backgroundColor: '#. There are still a handful; convert to theme tokens. - Logo + brand — see P1 #12. Then once the name is settled, the "DH" logo + AnimatedFireLogo need to be unified into one brand mark.
- Modal patterns —
CollectionSelectionModal,ShareModal,UploadImageModaleach have their own backdrop + focus-trap implementation. Extract<Modal>primitive. Useheadlessuiorradix-ui's Dialog to get focus management for free. - Card grid spacing + density —
pages/cards.js(the 1499-line monster) does responsive grid math inline. Extract a<CardGrid>component that handles density (compact / comfortable / spacious) + sort + filter chrome.
Role-a11y-auditor findings
- Focus traps in modals — none of the modals trap focus. Tab through
ShareModaland you leave to the background. Critical for keyboard users + screen readers. - ESC to close modals — inconsistent. Some have it, some don't.
- Skip-to-content — no
<a href="#main" class="sr-only focus:not-sr-only">. Add to_app.js. - Image alts — card images use
alt={card.name}(good); avatar images sometimes have empty alts. Audit. - Color contrast — verify the muted text colors (
var(--text-secondary)) hit AA on both themes. The mobile bottom-bar inactive state is a likely fail. - Form errors — login/signup form errors are visually red but not connected to inputs via
aria-describedby. Screen readers don't know which field failed. - Keyboard ops on non-button elements — most clickable
<div>s already haveonKeyDownbut a few don't (audit withrg "onClick" components pages | rg -v "<button").
Role-doc-writer findings
- README — needs a public-facing rewrite. Currently mixes user docs + dev setup + admin credentials. Split into
README.md(project landing) +docs/DEVELOPMENT.md(dev setup) + delete the admin credentials section entirely. docs/SCHEMA_MAP.md— installed at bootstrap (this convoy). Keep it fresh on every schema change.- CHANGELOG — none yet. Adopt Keep-a-Changelog format. Backfill
[0.1.0] — initial private alphacovering everything to date. TESTING_GUIDE.md— currently the only test doc; rename todocs/MANUAL_QA.mdoncevitest+playwrightland.docs/API_REFERENCE.md— would help. Could be auto-generated by walkingpages/api/**/*.jsand extracting JSDoc; or hand-curated to start.- Privacy policy / Terms of service — required before public launch. Use a template (Termly / Iubenda) and customize.
Proposed launch sequence
Each phase is one Conductor-created convoy. Don't run more than two in parallel until tests exist.
bump-next-js(P0 #8). One PR. MUST land first — Vercel is currently blocking all deployments, which makes every other PR's preview-smoke / visual-diff gate non-functional. Trivial bump; risk is breaking changes if going to 16.x.fix-auth-bypass(P0 #1, #2, #4, #5, #6 partial). One PR. Highest risk; needs human review.drop-public-setup(P0 #3, #4). One PR. Trivial; do as a hotfix.fix-layout-default-user(P0 #7). One PR. Trivial. 3.5.fix-lint-baseline(P1 #11.5). 2-4 PRs via multitask. RESOLVED 2026-06-02 — PRs #61–#63; lint 0 problems; CI lint blocking.add-rate-limiting(P0 #6 full). One PR. Adds @upstash/ratelimit + applies to listed routes. RESOLVED 2026-05-24 — PR #20 squash708ef45; closes P0 #6 (last open P0), flipping the ship-blocker set to 8/8 RESOLVED. 5 named limiters (auth/search/upload/generate/import), 6 routes newly gated + the 3 import routes auth-gated atomically with apages/admin/card-import.jsBearer-header fix. Smoke 3/3 green in 3.8s post-merge — confirms the new 60/min search limiter doesn't 429 the smoke spec. See § Queued convoys and P0 #6 above for the full as-shipped block.pick-a-name(P1 #12). Human decision first, then one or two PRs. RESOLVED 2026-05-24 — PR #21 squash9abbab6; closes P1 #12 (brand-consistency, the inconsistencyAGENTS.mdline 5 had flagged since project setup). Two file-disjoint briefs landed serially (B1 commitac8c998display + comment sweep across 7 files; B2 commit1c18d21infrastructure + email migration across 10 modified + 1 new migration script). Five canonical-string D-decisions ratified verbatim at gate-1 (Deck Hearth /deck-hearth/deckhearth/admin@deckhearth.com/ fulldeckhearthRedis prefix) plus Risk 4 PRESERVE ontest/lib/permission-middleware.test.jsline 87's negative regression-lock literal. Smoke 3/3 green in 1m4s post-merge — fourth convoy in a row (PR #15 → #19 → #20 → #21) where the same 3-test smoke spec defends the auth surface through a sweeping change. Operator action required: runnode scripts/migrations/2026-05-24-rename-admin-email.jsagainst prod Neon BEFORE the next admin login attempt with the new email (idempotent, UNIQUE-collision-safe). See § Queued convoys for the downstreamrename-repo-and-vercel-project+point-domain-at-deckhearth+regenerate-brand-assetsfollow-ups, and.convoys/pick-a-name.md§ As-shipped for the full record.adopt-vitest(P1 #10 step 1). One PR. Enables testing every future change.migration-tool(P1 #11). One PR. RESOLVED 2026-05-26 — PR #32 squashde9f334; closes P1 #11 (no migration tool).node-pg-migrate@^8adopted with the initial schema backfilled tomigrations/1779853647564_initial-schema.js;setup-neon-db.jsnow owns env-var validation + migration-runner spawn + admin-row seed only. The 27+ historicalscripts/add-*.js/fix-*.js/seed-*.jsgraveyard is preserved per no-go-zones; new schema changes go throughnpm run migrate create. All 7 architect decisions ratified verbatim at gate 1. Smoke 3/3 green post-merge (sixth consecutive convoy in the cross-validation lineage). See § P1 #11 above for the full as-shipped block.single-sql-client(P1 #8). 2-3 PRs, fanned out via multitask once per-file briefs are written. RESOLVED 2026-05-26 — PR #30 squashc403ea4; closes P1 #8 (two SQL clients in parallel).lib/database.jsdeleted; the 2 callers (pages/api/auth-utils.jssource +test/api/auth-utils.test.jsmock cleanup) migrated to@vercel/postgrestagged templates. The "2-3 PRs via multitask" estimate was loose — actual surface was a single 4-file PR (architect grep confirmed exactly 2 callers, not the ~3 from graph).@neondatabase/serverlessretained because 11scripts/*helpers still useneon()directly (deferred to the newly queuedpurge-neondatabase-serverless-fully, unblocked by step 7 above). Lint baseline preserved at 125 (the new post-PR-#31 baseline). See § P1 #8 above.single-auth-provider(P1 #9). 3-5 PRs via multitask. RESOLVED 2026-05-26 — PR #31 squash0668b0c; closes P1 #9 (three parallel client-side auth implementations).lib/auth-context.js+lib/admin-auth.jsdeleted; 7 source files swept (pages/_app.js,pages/index.js,pages/scanner.js,pages/decks.js,pages/deck/[id].js,pages/deck-builder.js,pages/card/[id].js);<AuthProvider>wrapper removed from_app.js;useIsAdmin()'s lone consumer (pages/card/[id].js) inlined asuser?.role === 'admin'. Verify-roundtrip count reduced 3 → 1 oncard/[id].jsmount, 2 → 1 on every other page-load. Pre-merge importer estimate was ~30; actual was 7 becausefix-layout-default-user(PR #15) had already migrated most of the tree. The "3-5 PRs via multitask" estimate collapsed to a single atomic PR for the same reason. Lint baseline improved 128 → 125 (3 fewer errors from deleted unused-import / unused-var lines in the deleted files); this is the new lint baseline. See § P1 #9 above.adopt-playwright-smoke(P1 #10 step 2). One PR. RESOLVED 2026-05-24 — PR #18 squash7b6f751; smoke 3/3 green in 2.9s, full workflow 59s, zero secret leaks. See § Queued convoys for the full as-shipped block.schema-cleanup(P2 #14). Multi-PR convoy via multitask.god-component-split(P2 #13). One convoy per file; fan out via multitask once architect'sslice_dependenciesare written.launch-polish(P3). UX/IA/a11y/docs convoy.
Total: ~14 convoys to get from current state to public-launch-ready. Estimate 4-8 weeks at one human-in-the-loop reviewer per convoy. Multitask + Cursor 3.2 worktrees compress steps 8-12 substantially.
Queued convoys
Follow-ups surfaced mid-convoy or mid-PR that didn't fit the original launch sequence but need to land before public traffic. Listed in priority order; not all will be P0/P1 — most are CI / DX / hygiene polish.
-
rotate-default-admin(priority: P2 hygiene). Operator-rotation script for envs that ransetup-neon-db.jsbeforedrop-public-setupand still carry the weakadmin123bcrypt hash. Surfaced in P0 #3 § Operator caveat. Optional: do nothing if no audit finds a deployed env with the weak hash. -
delete-dead-lorcana-import— RESOLVED 2026-06-02 by PR #59 (8262fec). Deletedpages/api/cards/import-lorcana.jsandscripts/import-lorcana.js; no dedicated convoy file (cleanup tracked here only). Entry kept for audit trail. -
tighten-visual-diff-path-filter— RESOLVED 2026-05-26 bytighten-visual-diff-path-filterconvoy, squash commitba95462(PR #26). Single-editpaths:filter change in.github/workflows/visual-diff.yml: inserted'!pages/api/**'immediately after'pages/**'(order-sensitive per GitHub Actions' minimatch path-filter semantics — exclusions only fire after a prior include matches). Verified the YAML deserialization order at gate time (['pages/**', '!pages/api/**', 'components/**', 'styles/**', 'tailwind.config.js', 'postcss.config.js']).preview-smoke.ymlleft untouched (nopaths:filter; intentionally fires on every PR). Diff: 2 files, +279 / -0 (1 YAML entry + inline comment block + the planning convoy file). Post-merge verification still pending — the only true verification is that the next API-only PR after this merges does NOT triggerScreenshot diff. PR #30 (single-sql-client, squashc403ea4) was the first API-only PR post-merge and its CI Checks tab showedScreenshot diff: not triggered— empirical confirmation that the!pages/api/**exclusion fires correctly. The next-API-only-PR success line was originally specified in the convoy file's § Verification plan as the deferred-to-post-merge gate; this is that confirmation. Entry kept (not removed) to preserve the audit trail. See.convoys/tighten-visual-diff-path-filter.md§ As-shipped. -
purge-weak-creds-from-helpers— RESOLVED 2026-05-26 bypurge-weak-creds-from-helpersconvoy, squash commit5f2b234(PR #27). The umbrella is now closed; both remaining halves shipped together. Multi-convoy history: (1)drop-public-setupBrief 1+2 (ff80753+b63b509) removed the firstadmin123literal fromscripts/setup-neon-db.jsand set the env-var + fail-loud + no-echo precedent. (2)pick-a-nameBrief 2 (9abbab6) swept the@tcgvault.comliterals in the three helper paths to@deckhearth.comtogether with the migration script. (3)fix-reset-db-script(3ab9bf8, PR #25) removed the secondadmin123fromscripts/reset-db.jsand the secondAdmin Password:echo. (4) This convoy (PR #27) closes the umbrella by sweeping the last two files:scripts/create-test-users.js(alice/bob fixtures, previously hardcodingbcrypt.hash('alice123', 12)+bcrypt.hash('bob123', 12)and echoing both literals to stdout) andTESTING_GUIDE.md(Test Accounts table previously documenting the weak literals). The post-convoy contract: singleTEST_USERS_PASSWORDenv var (intentional simplification per Risk R2 — these are collaboration-flow demo fixtures, not independent identities), fail-loud at the top ofcreateTestUsers()BEFORE any DB connection, no password echo anywhere (✅ Created Alice (alice@deckhearth.com / alice123)→✅ Created Alice (alice@deckhearth.com)),ON CONFLICT (email) DO NOTHINGpreserved. Diff: 3 files, +249 / -22. Lint preserved at 125 (post-PR-#31 baseline); vitest 21/21. ESM-already (this was the first of the three weak-creds-shape convoys to skip the CJS→ESM half becausescripts/create-test-users.jswas already top-level ESM). Operator caveat: existing alice/bob rows in already-seeded envs are NOT rotated by re-running the script —ON CONFLICTpreserves the old hashes; operators must rotate manually via the app or drop those rows and re-seed. Same caveat as thedrop-public-setupadmin-row guidance. Surfaced out-of-scope follow-up:purge-quick-login-from-loginpage— see new queue entry below. Entry kept (not removed) to preserve the audit trail. See.convoys/purge-weak-creds-from-helpers.md§ As-shipped. -
rename-repo-and-vercel-project(priority: P2 polish). Rename the GitHub repo + the Vercel project fromtcg-vaulttodeck-hearthto match the canonical product brand ratified inpick-a-name(squash9abbab6, 2026-05-24). Auto-redirects on both GitHub and Vercel make this low-urgency; the surface is a one-line update to local git remotes (git remote set-url origin git@github.com:<owner>/deck-hearth.git) + a Vercel project-settings rename + the 8 architect-verified literal-repo references documented in.convoys/pick-a-name.md§ Full surface inventory § Repo / Vercel project name (out-of-scope) —README.mdlines 30 + 105,AGENTS.mdline 1,.github/workflows/ci.ymllines 12 + 123,.github/workflows/visual-diff.ymlline 5,.agent-context-manifest.ymlsource tags. Also re-evaluate the.agent-context-manifest.ymlsource: "tcg-vault-local"tag at that point (Risk 5 ofpick-a-name— renaming the source tag could break thesync-agent-contextskill's drift tracking; do this convoy with the sync-skill author's input). Surfaced 2026-05-24 as the explicit downstream ofpick-a-name. -
point-domain-at-deckhearth(priority: P2 polish; blocked on domain acquisition). Once the operator buysdeckhearth.com(or.app/.gg/ other), wire DNS to the Vercel deployment + claim the domain in Vercel's project settings + update the seed admin email's TLD if the purchased TLD is anything other than.com(a one-line REPLACE migration mirroringscripts/migrations/2026-05-24-rename-admin-email.js). Surfaced 2026-05-24 as the explicit downstream ofpick-a-name(the convoy seed § "DNS / domain — out of scope — separate convoypoint-domain-at-deckhearth(you don't own adeckhearth.*domain yet per operator's pre-convoy statement)"). Until this lands, the seed admin emailadmin@deckhearth.comis a placeholder STRING used as a unique identifier — auth uses email as identity, not as a mail target, so no working mailbox is required for login to function. -
regenerate-brand-assets(priority: P2 polish). Regenerate the favicon (public/favicon.ico), Open Graph images, and social share cards with the new Deck Hearth identity. Requires a design pass — out-of-scope for any single agent-driven convoy; queue when an asset-design pass is scheduled. Surfaced 2026-05-22 originally in.convoys/ship-readiness.md§ Role-design-system-auditor findings ("Then once the name is settled, the 'DH' logo + AnimatedFireLogo need to be unified into one brand mark"); reaffirmed 2026-05-24 inpick-a-nameOut-of-scope queued follow-ups. -
convert-reset-db-to-esm— RESOLVED 2026-05-26 byfix-reset-db-script(squash3ab9bf8, PR #25).scripts/reset-db.jsnow uses ESM top-level imports (import dotenv,import { neon },import bcrypt) and executes cleanly on Node 22.x. Same fix shape assetup-neon-db.jspost-drop-public-setupB2. As predicted in this entry's prior note, the fold-with-purge-weak-creds-from-helpersshape was the right call — both ailments inscripts/reset-db.jswere fixed atomically with a single 55-line diff. Entry kept (not removed) to preserve the audit trail. See.convoys/fix-reset-db-script.md§ As-shipped. -
lint-against-cjs-in-esm-scripts— RESOLVED 2026-05-26 bylint-against-cjs-in-esm-scriptsconvoy, squash commit13d6210(PR #29). Single 7-line flat-config block added toeslint.config.mjsafter the existingglobalIgnores(...)call:{ files: ['scripts/**/*.js'], rules: { 'no-restricted-syntax': ['error', { selector: 'CallExpression[callee.name="require"]', message: '...' }] } }. The error message points at.convoys/fix-reset-db-script.mdso a future contributor who trips the rule gets a 1-click path to the exemplar ESM fix shape. Scope decision:scripts/**only, NOT all.js(matches actual blast radius — every observed bug instance has been in a helper script; the config filespostcss.config.js/tailwind.config.jslegitimately use CJS-style exports that the next-config base rules already handle correctly). Would have caught bothdrop-public-setupBrief 2's pre-fixscripts/setup-neon-db.jsANDfix-reset-db-script's pre-fixscripts/reset-db.jsat lint time instead of at first execution — the exact two motivating bugs from the multi-convoy history. Diff: 2 files, +185 / -0. Lint baseline preserved at 125 (post-PR-#31; zero new false positives in the current tree because both motivating bugs were already fixed). Negative test verified: prependingconst x = require('fs');toscripts/reset-db.jsfires the rule at the expected line/column with the documented message; reverting returns to a clean lint.scripts/migrations/**was already inglobalIgnores(frompick-a-nameBrief 2's migration script); the rule does not fire there. Entry kept (not removed) to preserve the audit trail. See.convoys/lint-against-cjs-in-esm-scripts.md§ As-shipped. -
single-auth-provider— RESOLVED 2026-05-26 bysingle-auth-providerconvoy, squash commit0668b0c(PR #31; also listed as launch sequence step 9 and § P1 #9 above — both flipped to RESOLVED in the same wave).lib/auth-context.js+lib/admin-auth.jsdeleted; 7 source files swept;<AuthProvider>wrapper removed from_app.js;useIsAdmin()'s lone consumer inlined asuser?.role === 'admin'. Importer inventory was 7, not the ~30 estimated in P1 #9 (most of the tree was already onlib/use-auth.jspost-fix-layout-default-user). Lint improved 128 → 125. Entry kept (not removed) to preserve audit trail. See § P1 #9 + launch sequence step 9 above for the full as-shipped block. -
cleanup-mobile-nav-dead-props— RESOLVED 2026-05-26 bycleanup-mobile-nav-dead-propsconvoy, squash commit171f5af(PR #28). Two-file, three-line diff: (1)components/MobileNavigation.jsline 5 —{ user, onMenuOpen }→{ onMenuOpen }; (2)components/Layout.jslines 598-601 — removed theuser={user}JSX attribute from the only active call site. Audit confirmeduserwas genuinely dead pre-fix (the bottom-bar items are static and don't depend on auth state).components/Layout.js.backupleft untouched per the.cursor/rules/no-go-zones.mdc§ "Append-only / historical" rule (its staleuser={user}call disappears when the.backupfile is eventually deleted in a separate convoy). Diff: 3 files, +156 / -2 (3 lines source + the planning convoy file). Lint preserved at 125; vitest 21/21 (thetest/components/Layout.test.jsregression-lock assertions for the logged-out Layout branch do not assert onMobileNavigation's prop shape, so the dead-prop removal is invisible to the suite). Pre-existing deadimport { useState } from 'react'atMobileNavigation.jsline 3 left untouched per the convoy spec's "single-prop removal" boundary. Did NOT fold intogod-component-split(P2 #13) — that hasn't landed yet, so this small hygiene convoy shipped first. Entry kept (not removed) to preserve audit trail. See.convoys/cleanup-mobile-nav-dead-props.md§ As-shipped. -
bump-eslint-10(priority: P2 hygiene; upstream-blocked). Bump ESLint from v9 to v10 oncetypescript-eslintships a v10-tested release andeslint-config-nextbundles it. Surfaced in.convoys/bump-next-js.md§ Decisions D. -
harden-multipart-parser(priority: P2 quality). Surfaced 2026-05-24 inadd-rate-limiting§ Risk list.pages/api/user/avatar.js'sparseMultipartFormDataconsumes the 5MB multipart body viareq.on('data')before any response is sent, so an attacker can still exhaust the 5MB body even on a 429 path from the newcheckUploadRateLimitgate. Real defense requires moving the parse into a separate edge function or usingread-up-tosemantics. Not a release-blocker — the gate-ordering in PR #20 places the limiter BEFORE the method branches that callparseMultipartFormData, so when this hardening lands, the gate ordering is already correct. Surface as P1 only if a real abuse incident occurs. -
god-function-split/refactor-cards-search-sql(priority: P2 refactor). Surfaced 2026-05-24 inadd-rate-limiting§ Files explicitly out of scope.pages/api/cards/search.jshas a 240-line god-function shape with 7+ conditionalSELECT * FROM cards WHERE …branches; the PR #20 rate-limit gate sits at the top of the handler and leaves the SQL byte-identical. Splitting is its own scope (probably one convoy per branch group withslice_dependencies:for safe multitask fan-out). Not security-critical; deferred to the P2 lane. -
withAdmin(handler)wrapper extraction (priority: P3 polish / DX). Surfaced 2026-05-24 inadd-rate-limitingDecision 1 + § What did NOT change..cursor/rules/auth-and-permissions.mdcnotes "checkuser.role === 'admin'directly; consider extractingwithAdmin()if a third call site appears" — the threecards/import-*.jsroutes are the third+fourth+fifth call sites in the codebase, but PR #20 kept the inline shape for uniformity across the three import routes and for the convoy's atomic-close-P0-#6 goal. A future convoy can extractwithAdmin(handler)tolib/permission-middleware.js(or wherever the architect decides) and sweep all 5 admin-role check sites onto it. Pure refactor; no security delta either way. -
seed-visual-baselines-on-linux— RESOLVED 2026-06-02 by PR #58 (83a358b). Linuxtests/visual/__screenshots__/home.pngcommitted;Screenshot diffcan now compare on UI-touching PRs. Entry kept for audit trail. -
adopt-playwright-smoke(priority: P1 quality, also listed as launch sequence step 10 / P1 #10 step 2) — RESOLVED 2026-05-24.- Resolved by: squash commit
7b6f751(PR #18, architect-commit3ac527e, implementer-commitc72d006). Brief 1 shipped as planned with two small lint-baseline-preserving deviations from the brief's verbatim shape (documented in the convoy file's § As-shipped). - As-shipped surface:
@playwright/test@^1.60.0added todevDependencies; newplaywright.config.jsat repo root (ESM, two projects partitioned bytestMatch—smoke+visual, CI-fail-loud / dev-warn predicate onVERCEL_AUTOMATION_BYPASS_SECRETper Decision 2,snapshotPathTemplate: 'tests/visual/__screenshots__/{arg}{ext}'aligned withvisual-diff.yml's artifact upload path); newtests/visual/homepage.spec.ts(1 test, no baseline committed per Decision 4); three newpackage.jsonscripts (test:smoke,test:visual,test:visual:update); three new.gitignoreentries (/playwright-report/,/test-results/,/.playwright/). Noeslint.config.mjschange (Decision 5 + Finding 2 verified clean empirically). No source touched underpages/**/components/**/lib/**. - Implementer deviations (both behavior-neutral, both lint-baseline-preserving):
- Removed Brief 1's
// eslint-disable-next-line no-consoledirective onplaywright.config.js'sconsole.warnbranch — the current ESLint config does not flagconsole.warnat all, so the disable directive itself would have regressed lint from 128 → 129 as an "Unused eslint-disable directive" error. - Placed
@playwright/testfirst indevDependenciesfor strict alphabetical correctness — the brief's prose was internally inconsistent on neighbors (@playwrightsorts lexically before@testing-library/react).
- Removed Brief 1's
- As-shipped metrics (from post-merge
Playwright smokerun 26376162598 onmain):Playwright smokeworkflow total runtime: 59 seconds, exit 0 (was: fast-fail at "playwright not installed" / "no config" before this convoy).Run smoke testsstep: 3/3 tests pass in 2.9s against the Vercel preview withx-vercel-protection-bypassheader applied —home redirects or renders without 5xx✓ 683ms /sign-in page renders✓ 459ms /public health endpoint responds✓ 571ms.Screenshot diffworkflow: not triggered on PR #18 itself because itspaths:filter excludes test-infra-only changes; first real trigger fires on the next PR touchingpages/**/components/**/styles/**/tailwind.config.js/postcss.config.js. At that point the documented Decision-4 end state runs live (test fails on missing baseline →continue-on-error: trueswallows → comment-on-PR step posts run link with empty artifacts).- Bypass secret leak check: 0 matches in the raw workflow log. GitHub Actions auto-masks registered secrets; our Decision-2 branches name the env var but never interpolate the value into any string.
- Cross-validation finding (not a planned AC; surfaced organically from CI green): smoke test 2 (
'sign-in page renders') assertsawait expect(page.getByRole('button', { name: /sign in/i })).toBeVisible()against/login, which only passes becausecomponents/Layout.jsrenders the<Link href="/login">Sign in</Link>CTA on the logged-out branch that PR #15 (fix-layout-default-user,ca302a8) introduced. P0 #7's resolved state is now defended by a live CI signal — if a future PR reverts to a hardcoded default user or breaks the CTA wording, smoke fails the PR (in addition to the 5 vitest assertions intest/components/Layout.test.js). - Operator action required going forward: none for smoke.
seed-visual-baselines-on-linuxRESOLVED PR #58 —Screenshot diffnow has a Linux baseline for homepage. - Flagged-but-deferred (deliberately out of scope per the convoy file, restated here for the audit trail):
— RESOLVED PR #58.seed-visual-baselines-on-linuxadopt-test-smoke-local(possible follow-up) — atest:smoke:localwrapper that auto-bootsnext dev. Explicitly rejected by Decision 6; queue only if dev friction proves out.- Deeper E2E coverage beyond the 3 existing smoke checks — per-feature work in feature convoys, not a test-infra concern.
- Owns:
role-architect(3 of 6 decisions self-ratified — D2 CI predicate, D3 two-project shape, D5 no-eslint-change; 3 of 6 operator-ratified — D1 keep.ts, D4 defer baselines, D6 simple scripts) →role-implementer(Brief 1, plus the two deviations above).
- Resolved by: squash commit
-
fix-vercel-deployment-protection-in-ci(priority: P2 CI infra) — RESOLVED 2026-05-24.- Resolved by: squash commit
9a3e077(PR #17), comprising three commits, not one. Operator prereq seeded 2026-05-24T20:03:31Z (gh secret set VERCEL_AUTOMATION_BYPASS_SECRET; confirmed viagh secret list); the implementer dispatch waited on that visibility per the convoy file's "Operator action required" gate. - Three-commit reality (Brief 1 + two scope expansions found during CI validation):
365e9f0Brief 1 — bypass plumbing per spec. Both.github/workflows/preview-smoke.ymland.github/workflows/visual-diff.ymlgot the same shape change:wait-for-vercel-preview@v1.3.2'spath:input now carries/?x-vercel-protection-bypass=${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}&x-vercel-set-bypass-cookie=true(Decision A's original cookie-variant shape — later corrected in commit 3);max_timeout: 600 → 120(Decision B); thegate:job's Decide step short-circuits ongithub.event.pull_request.head.repo.fork == truewith a::notice::annotation, before the existing PR-body skip directive runs (Decision D); and the Playwright/screenshot step exportsVERCEL_AUTOMATION_BYPASS_SECRETasenv:for forward-compat withadopt-playwright-smoke.b6f8688shell-injection hardening (latent pre-existing bug surfaced by PR #17's own CI validation). Decision D's gate step inlined${{ github.event.pull_request.body }}directly into bash, which broke when the PR body contained shell metacharacters like(or backticks. PR #17's own description bit this with"unexpected token \('"because of phrasing like *"(was: 10-minute timeout)"*. Fix is the standard GitHub Actions hardening pattern (their official "Security hardening" guide flags inline${{ }}in shell as both a syntax-error risk and a shell-injection vector): route the body and the fork flag through the step'senv:block asPR_BODYandPR_IS_FORK, then quote them as"$PR_BODY"/"$PR_IS_FORK"` in the shell condition. Same change in both workflows; ~9 LOC each. Documented in commit message as technically beyond Brief 1's scope but bundled into the convoy because the bug actively blocked Brief 1's success criterion from being validated.043a6eedrop&x-vercel-set-bypass-cookie=truefrom the wait-actionpath:— corrects Decision A's exact shape. With the cookie variant, Vercel responds 307 + Set-Cookie, and axios in Node has no cookie jar — it follows the redirect to the bare URL without the cookie, which then 401s. Empirically confirmed by operator's local curl: bare?x-vercel-protection-bypass=X→ HTTP/2 200, while?x-vercel-protection-bypass=X&x-vercel-set-bypass-cookie=true→ HTTP/2 307 (the broken path). 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. An inline comment inpreview-smoke.ymlexplains this so the next agent doesn't accidentally re-add the cookie param.
- As-shipped metrics (from PR #17's CI run, post-validation):
Wait for Vercel Preview deploymentstep elapsed: 194 milliseconds (was: 10-minute timeout before this convoy).Playwright smokeworkflow total runtime: 59 seconds (was: 10+ minutes).- Step breakdown:
Wait for Vercel Preview deployment→ success in 194ms;npm ci,setup-node,playwright install→ success;Run smoke tests→ failure (expected — see next bullet). Screenshot diffworkflow: not triggered on PR #17 itself because its path filter excludes workflow-only changes; will fire on the next PR touchingpages/**/components/**/styles/**/ Tailwind/PostCSS config.
- Documented expected red:
Playwright smokenow reachesnpx playwright testand fast-fails becauseplaywright.config.jsdoesn't exist in the tree yet. That isadopt-playwright-smoke's scope (P1 #10 step 2 / launch sequence step 10), not this convoy's. Per the convoy file's Test plan § and Brief 1 acceptance criterion #1, a real downstream failure with the wait-action reachingReceived success status codefirst counts as success for this convoy — the failure mode shifted from "401 timeout in the wait step" to "playwright not installed", which is precisely the target state. - Operator-rotation caveat (R6 in the convoy file). The Vercel bypass token does not auto-expire. If/when it's rotated from the Vercel dashboard, the operator must re-seed the GitHub secret via
gh secret set VERCEL_AUTOMATION_BYPASS_SECRET --body "<new value>". Same human-responsibility pattern asJWT_SECRETrotation; not preventable from workflow YAML. No automation here. - Flagged-but-deferred (from the convoy file's "Anything flagged but not acted on" section, unchanged at merge):
replace-wait-for-vercel-preview— the wait-action's last release was Mar 2024; could be replaced with a few lines ofgh api+curl-loop. Out of scope for this convoy; queue if the action ages out further or gets a security advisory.adopt-playwright-smoke— owns the actualplaywright.config.js,tests/smoke/, and@playwright/testdep. The bypass plumbing here is forward-compat for that convoy (env var available on the smoke step). Listed in P1 #10 step 2 / launch sequence step 10 above.Screenshot diffbaseline authoring — orthogonal scope; the visual-diff workflow has nothing to compare against on its first real run.
- Owns:
role-architect(3 Decisions ratified — A query-param, B 120s timeout, D fork-PR skip) →role-implementer(Brief 1) + two scope-expansion commits.
- Resolved by: squash commit
-
purge-quick-login-from-loginpage— RESOLVED 2026-05-29 by PR #56 (e0218e4). Quick Login removed frompages/login.js. See.convoys/purge-quick-login-from-loginpage.md§ As-shipped. -
purge-neondatabase-serverless-fully— RESOLVED 2026-06-02 by PR #57 (5115683).@neondatabase/serverlessremoved frompackage.json; operational scripts use@vercel/postgres. Historicalscripts/add-*/fix-*/seed-*graveyard unchanged per no-go-zones. Entry kept for audit trail. -
wire-migrate-into-ci(priority: P2 CI infra). Surfaced 2026-05-26 bymigration-tool(PR #32) — D6 deferral. Add a CI job that runsnpm run migrate upagainst a test DB (either a dedicated Neon branch +MIGRATE_TEST_DATABASE_URLsecret with branch-reset logic, or a Postgres service container with a ~30s container-start tax). Catches syntactically-invalid migrations + most logical errors at PR time. Currently, the first signal that a new migration is broken is the developer's localnpm run migrate upagainst their dev branch (or post-deploy on Vercel). Documented in.convoys/migration-tool.md§ R3. -
reconcile-historical-add-scripts(priority: P1 quality — needed for fresh-env onboarding). Surfaced 2026-05-26 bymigration-tool(PR #32). Fold the effects of the 27 historicalscripts/add-*.js/fix-*.js/seed-*.jsjobs into the migration history so a brand-new Neon branch can be onboarded bynpm install→npm run setup-dbalone (without manually replaying the historical scripts). Multi-PR; ideally one migration per logical change, generated by reading the scripts' SQL and re-shaping into idempotentpgm.sql(...)blocks (withIF NOT EXISTS/IF EXISTSguards so re-application is safe). Documented in.convoys/migration-tool.md§ R1. -
retire-graveyard-scripts-after-audit(priority: P3 polish; blocked onreconcile-historical-add-scripts). Surfaced 2026-05-26 bymigration-tool(PR #32). Once the migration history captures all historical effects, the legacyscripts/add-*.js/fix-*.js/seed-*.jsfiles can be deleted (or moved toscripts/historical/). They remain no-go-zones until that cleanup convoy lands. Documented in.convoys/migration-tool.md§ Follow-ups. -
audit-node-pg-migrate-transitive-deps(priority: P3 hygiene). Surfaced 2026-05-26 bymigration-tool(PR #32) — R5 in the convoy file.npm auditreports 11 vulnerabilities (6 moderate, 5 high) coming fromnode-pg-migrate@8.0.4'sglob@~11.1.0+yargs@~17.7.0transitive deps (olderbrace-expansion,minimatch,picomatchversions with known advisories). All in dev-only paths; the migration tool runs in scripts/CI, never in the deployed Next.js bundle, and the affected APIs (glob's shell-injection CLI; brace-expansion's ReDoS) are not exercised by node-pg-migrate's call sites. Surface only if a security audit specifically flags this surface, or ifnode-pg-migrateships a v9 that updates the transitive tree. -
add-migration-template(priority: P3 DX). Surfaced 2026-05-26 bymigration-tool(PR #32). Add a custom template via--template-file-nameso generated migrations include the project's preferred docstring shape + a reminder aboutdocs/SCHEMA_MAP.mdupdates. Surface if migration authoring proves inconsistent.
Scanner audit portfolio (2026-05-27)
Six convoys authored from the scanner audit portfolio plan. Dependency order:
secure-scanner-gemini-key → server-side-scan-pipeline → (add-real-ocr-layer ∥
redesign-scanner-flow); rename-collections-vocabulary and
scanner-correctness-polish parallel after #1.
secure-scanner-gemini-key— RESOLVED 2026-05-27 — PR #34 (8c58990). Client key leak closed;forbidden-client-side-llm-keysCI gate. Convoy:.convoys/secure-scanner-gemini-key.md.server-side-scan-pipeline— RESOLVED 2026-05-27 — PR #35 (e81dd49). Server-owned identify,card_submissions, disambiguation. Convoy:.convoys/server-side-scan-pipeline.md.add-real-ocr-layer— RESOLVED 2026-05-27 — PR #38 (d798e28) + polish PR #39. Layer-1 Tesseract +pg_trgm. Convoy:.convoys/add-real-ocr-layer.md.redesign-scanner-flow— RESOLVED 2026-05-27 — PRs #42 (Brief 1), #43 (Brief 2), #44 (Brief 3). Post-PR auditaudit-redesign-scanner-flow-44posted to PR #44; outcome comment-only. See.convoys/redesign-scanner-flow/audit-redesign-scanner-flow-44.md. Follow-ups:scanner-redesign-a11y-fixesRESOLVED PR #45;scanner-user-cards-quantity-guardRESOLVED PR #46;test-scanner-redesign-surfacesRESOLVED PR #47.god-component-split/CameraScanner.jsslice — RESOLVED 2026-06-02 — PRs #67–#72 (Briefs 1–5) + view extract (CameraScannerView.js). Pre-split ~1,050 lines → ~45-line composer + presentational view. Remaininggod-component-splittargets:pages/cards.js,pages/scanner.js, etc. (see § P2 #13 table).rename-collections-vocabulary— RESOLVED 2026-05-29 — PR #54 + follow-up PR #55. Convoy:.convoys/rename-collections-vocabulary.md.scanner-correctness-polish— RESOLVED 2026-05-27 — PR #41 (55af7e3). Convoy:.convoys/scanner-correctness-polish.md.schema-cleanup-from-scanner-audit(priority: P2 schema; deferred — NOT scanner-specific). Separate convoy when ready; surfaced by the scanner audit but applies globally:cards.quantity+cards.favoritedon the global catalog — belong onuser_cards/user_favorites; drop fromcards.collections.is_publicvsvisibility— dual visibility flags; reconcile to one mechanism (see also P2 §14 in this file).is_system_collectionvsuser_cardsunification — ownership model smell; IA + schema convoy, not a scanner deliverable. Do not fold into the six scanner convoys above; queue as its own architect-led migration convoy after the scan pipeline stabilizes.
Catalog freshness (deferred — post-scanner)
catalog-sync-vercel-cron— RESOLVED 2026-05-29 — PRs #48–#52 (weekly Vercel Cron, shared import libs, admin trigger, submission auto-link, Pokémon data source switch). Convoy:.convoys/catalog-sync-vercel-cron.md.
Design-system redesign portfolio — Liquid Glass (2026-06-03)
Operator-requested epic to migrate the UI from the current "warm panel +
side-highlight + heavy gradient" visual language to a Liquid Glass
aesthetic that retains Deck Hearth's fireplace warmth as accent /
gradient / motion (not as panel fill). Umbrella convoy authored
2026-06-03; all 8 sub-convoys seeded as status: open awaiting
role-conductor refinement when picked up. This is not a launch
blocker — the 8 P0 ship-blockers are all RESOLVED — but it
dramatically raises the launch-day quality bar.
Umbrella convoy: .convoys/liquid-glass-redesign.md (the deep
dive — vision, hard scoping rules, dependency graph, risk register,
operator decision points).
Dependency-ordered sub-convoys:
liquid-glass-design-tokens(foundation, no UI change) —.convoys/liquid-glass-design-tokens.md. Adds glass surface / blur / rim-light / elevation tokens +docs/DESIGN_TOKENS.md. Strict blocker for all subsequent sub-convoys.liquid-glass-modal-and-surface-primitive—.convoys/ liquid-glass-modal-and-surface-primitive.md. Extracts<GlassSurface>+<Modal>primitives + sweeps all ~15 modals. Closes the ship-readiness "Modal patterns" + "Focus traps" findings. This is where modals start blurring the page behind them — the operator's core ask.liquid-glass-form-primitives—.convoys/ liquid-glass-form-primitives.md.<Button>,<Input>,<SearchBar>primitives + migration sweep. Closes the ship-readinessaria-describedbyfinding via<Input>'s error wiring.liquid-glass-layout-shell—.convoys/ liquid-glass-layout-shell.md. Layout sidebar + header + mobile bottom-bar onto glass. Highest-blast-radius PR in the portfolio. Closes the ship-readiness bottom-bar contrast finding.liquid-glass-card-surfaces—.convoys/ liquid-glass-card-surfaces.md.CardItem,CardDetailView,Card3D, rarity-glow reconciliation. Per-cardbackdrop-filterforbidden (perf budget).liquid-glass-public-and-auth—.convoys/ liquid-glass-public-and-auth.md. Landing editorial pass + auth pages + public collection/deck views. First-impression delivery.motion-system-pass—.convoys/motion-system-pass.md. Consolidates 12+ ad-hoc keyframes into a 4-tier motion taxonomy- reduced-motion enforcement + per-page budget. Parallel-safe with #2–#6.
cleanup-legacy-design-css—.convoys/ cleanup-legacy-design-css.md. Strict-deletion convoy: removesgradient-text-blue/purple/pink,glow-blue/purple/pink,accent-blue/purple/pinkaliases, hex sweep, CI grep gates to prevent regression. Ships last.
Multitask plan (from the umbrella's dependency graph):
- After #1 merges:
/multitask#2, #3, #7 (disjoint files). - Inside #2: multitask 4 modal-cluster briefs after Brief 1 lands the primitive.
- Inside #3: multitask 2 consumer-cluster briefs after Brief 1 lands the primitives.
- After #4 + #5 merge:
/multitaskper-page briefs in #6 (file-disjoint by route).
Per-sub-convoy gates: every sub-convoy fires preview-smoke.yml +
visual-diff.yml + lint + test: (vitest); per-PR post-merge
re-seeds Linux visual baselines via the seed-visual-baselines-on-linux
Docker workflow documented in AGENTS.md § 6.
Operator decisions tabled for the architect at sub-convoy #1's gate-1 (umbrella § Open questions for the operator):
- Glass tint strength (Apple-leaning vs Linear-leaning; default Apple-leaning).
- Light-theme glass base (warm white vs cool white; default warm).
- Dark-theme glass base (warm black vs cool black; default warm).
- Hover ember-rim intensity (subtle / pronounced).
- Drop
fire-glow-bgpage-background animation? (default: drop; retainember-floaton landing only.) - Sequencing under launch pressure: if shipping before the full epic completes, the MVP redesign is #1 → #2 → #4 (modals + Layout); then post-launch #3, #5, #6, #7, #8.
Status snapshot — full-portfolio drive-through 2026-06-03.
Operator-approved sweep landed the foundation + primitive kit + the
two highest-leverage surfaces (Layout shell + Modal + Form primitive
adoption on auth pages) + the discipline gates that lock in the new
design system. Vitest jumped 84 → 104 (+20 new primitive tests); lint
0 errors throughout; visual-diff baselines must re-seed in Docker
per AGENTS.md § 6 before subsequent UI-touching PRs land:
| # | Slug | Status |
|---|---|---|
| — | liquid-glass-redesign (umbrella) |
open — drives the portfolio |
| 1 | liquid-glass-design-tokens |
MERGED 2026-06-03 — 29 CSS vars + docs/DESIGN_TOKENS.md + AGENTS.md § Visual language |
| 2 | liquid-glass-modal-and-surface-primitive |
Brief 1 MERGED 2026-06-03 — <GlassSurface> + <Modal> + useFocusTrap + 10 tests; 4 reference modal migrations (ShareModal, CollectionDeleteModal, CollectionsCreateModal, CardDetailQuantityModal). Brief 2 (11 remaining modals: CollectionSelectionModal, CollectionsEditModal, CollectionsSuccessModal, CollectionEditModal, CardDetailDeckModal, ScanDisambiguationDialog, UploadImageModal, OCRSettings, ScannerPageView inline, pages/decks.js inline, plus any newcomers) queued — CI gate forbidden-modal-shell-without-primitive grandfathers these 9 files |
| 3 | liquid-glass-form-primitives |
Brief 1 MERGED 2026-06-03 — <Button> + <Input> + <SearchBar> + 10 tests; login.js + signup.js migrated (2 buttons + 7 inputs total). Brief 2 (profile/settings + deck-builder + scanner search + card-editor admin + collection-cluster modal-form bodies) queued |
| 4 | liquid-glass-layout-shell |
MERGED 2026-06-03 — 6 shell surfaces glass-migrated (desktop sidebar, mobile drawer, mobile overlay scrim, search header strip, UserProfileDropdown popover, MobileNavigation bottom bar). Layout regression-lock 5/5 preserved. |
| 5 | liquid-glass-card-surfaces |
architecture ratified 2026-06-03; implementation queued — pixel-sensitive (rarity-glow reconciliation) so wants a dedicated visual-diff baseline re-seed PR. Pre-blocked on a fix-card3d-state convoy (Card3D has pre-existing state-management bug). |
| 6 | liquid-glass-public-and-auth |
architecture ratified 2026-06-03; partial impl shipped via #3 (login + signup form primitives). Remaining: landing page editorial + public collection/deck views + login/signup outer-wrapper sweep |
| 7 | motion-system-pass |
MERGED 2026-06-03 — 8 motion tokens (5 durations + 3 easings) added; prefers-reduced-motion sweep upgraded from narrow to site-wide (universal selector w/ .motion-essential opt-in escape); docs/MOTION_SYSTEM.md authored |
| 8 | cleanup-legacy-design-css |
Brief 1 MERGED 2026-06-03 — 2 new CI gates (forbidden-modal-shell-without-primitive blocking; forbidden-deprecated-color-aliases warn-only audit baseline); .cursor/rules/ui-and-theming.mdc documents the primitive kit + canonical reference modals. Brief 2 (actual deletion of legacy aliases + utility classes + fire-glow-bg page background) queued for AFTER #2 Brief 2, #3 Brief 2, #5 Brief 1, #6 Brief 1 land. |
Vitest baseline after portfolio drive-through: 104 passing
(was 84 pre-portfolio). +10 from test/components/Modal.test.js; +10
from test/components/ui-primitives.test.js. The 5 Layout regression-lock
assertions (logged-out CTA, no maintainer-email default, "Sign in" link
present, supplied email renders, no "Guest" placeholder) all still
pass — every Layout edit preserved the documented contract.
What still needs human action before this lands in production:
- Squash + push the 8 PR-equivalent stacks (one per sub-convoy that shipped commits): #1, #2-Brief-1, #3-Brief-1, #4, #7, #8-Brief-1, plus the architecture-ratified #5 and #6 (no impl commits — just convoy + roadmap doc edits).
- Re-seed Linux visual-diff baselines via the Docker workflow (AGENTS.md § 6) after each UI-touching PR merges: #2 (modal reference migrations), #3 (login + signup form re-render), #4 (sidebar + header + drawer glass), and #7 (the universal motion sweep changes every transition's behavior under reduced motion, not its default render — likely a no-op for the baseline image, but verify).
- Verify
preview-smoke.ymlpasses against each preview deployment (the auth + scanner smoke specs touch login / signup / scanner — #3 + #4 most likely to surface a regression). - Operator-promote each merged-to-main commit to Vercel production via the Vercel dashboard (or auto-promote if the project is wired that way).
What still needs follow-up implementer turns to ship full polish:
- #2 Brief 2 — 11 remaining modal migrations (mechanical pattern copy from the 4 reference modals).
- #3 Brief 2 — profile + settings + deck-builder + scanner-search + card-editor admin form sweeps.
- #5 Brief 1 — card surface migration (gated on
fix-card3d-stateconvoy + a dedicated baseline re-seed). - #6 Brief 1 — landing page editorial + public view glass.
- #8 Brief 2 — actual legacy CSS deletion + CI gate graduation WARN → FAIL.
All five are documented inside their respective convoy files with specific file lists and decision rationale. None is launch-blocking — the user-visible promise of the epic ("modern fireplace aesthetic; modals blur the page behind them; reusable components") is delivered TODAY by the merged work.
Finish-portfolio sweep (2026-06-03 follow-up)
After the operator promoted PR #95 to production, the follow-up
finish-liquid-glass-design PR closed out the remaining briefs in
a single push. All 8 sub-convoys are now MERGED to main (only
the deferred Card3D state-management scope remains queued as its
own convoy, blocking #5 Brief 1's pixel-level rim migration).
| # | Slug | Final status |
|---|---|---|
| 2 | liquid-glass-modal-and-surface-primitive |
Brief 2 MERGED — every legacy fixed inset-0 bg-black bg-opacity- shell across pages/ + components/ migrated to <Modal>; CI grandfather list emptied to zero entries; the grep gate is now strict (no-allow-list) and bug-detects any reintroduction. Modals fully consolidated. |
| 3 | liquid-glass-form-primitives |
Brief 2 MERGED — <Button> + <SearchBar> adopted by dashboard, my-cards, community/collections, and CollectionsPageView; the remaining native button consumers in card-grid / per-row toolbars are intentionally left native (per-row tiny icon buttons whose styling doesn't match <Button> variants). |
| 5 | liquid-glass-card-surfaces |
scope-revised + MERGED — the planned migration was descoped after discovering components/Card3D.js was dead code (never imported from pages/** or components/**; only referenced in convoy docs). File deleted (-505 LOC). The actual card-grid component (components/CardItem.js) was intentionally left untouched in this sweep to preserve the per-rarity glow tuning that the visual-diff baseline locked in; a future implementer turn can apply rim-light tokens with a dedicated baseline re-seed. |
| 6 | liquid-glass-public-and-auth |
MERGED — pages/index.js landing editorial fully glass-migrated: three feature cards + featured-list cards now use <GlassSurface tint="mid" rim="subtle" elevation="ambient">, top nav got the --glass-surface-mid treatment matching Layout's sidebar, and all 6 CTA buttons are now <Button variant="primary"|"secondary">. pages/invite/{accept,decline}.js outcome panels wrapped in <GlassSurface> + all 8 buttons migrated to <Button>. |
| 8 | cleanup-legacy-design-css |
Brief 2 MERGED — every consumer of gradient-bg-purple (13 occurrences across 8 files) swept to gradient-bg-ember; the now-dead CSS class definitions for .gradient-text-blue, .gradient-text-purple, and the three [data-theme="dark"] .glow-{blue,purple,pink} selectors deleted from styles/globals.css; the forbidden-deprecated-color-aliases CI gate graduated from WARN to FAIL with all 9 patterns blocking. |
Vitest after the sweep: 104 passing (unchanged — primitive migrations don't add new unit tests; integration coverage is via Playwright smoke + visual-diff). Lint: 0 errors, 2 pre-existing warnings (unrelated CardEditorForm.js + CollectionsPageView.js carry-overs that were noted in PR #95).
CI graduation gates now blocking:
forbidden-modal-shell-without-primitive— zero allow-list; any newfixed inset-0 bg-black bg-opacity-shell fails the build.forbidden-deprecated-color-aliases— graduated WARN → FAIL; any new use of the 9 pre-Deck-Hearth alias classes fails the build.
Queued for a future convoy (no impact on the current ship-readiness state):
fix-card3d-stateis no longer needed — Card3D was dead code and is now deleted. The card-grid implementer turn that the original sub-convoy #5 envisioned can proceed againstcomponents/CardItem.jsdirectly when an operator wants the rim-light polish on cards, with its own visual-diff baseline re-seed.
Self-analytics
After each convoy, scripts/log-convoy-event.sh emits a record to .convoys/.metrics.jsonl (gitignored). After 3-5 convoys, run the upstream agent-pipeline/analytics/ aggregator to see where token spend goes — that data feeds whether to add or remove rules.
How to start
Per .cursor/agents/role-conductor.md, start the next convoy with:
"Run role-conductor: start a new convoy
fix-auth-bypassto address P0 #1, #2, #4, #5, #6 partial in.convoys/ship-readiness.md. Success =getUserFromRequestreturns null for missing tokens; no API route accepts unauthenticated requests; CI green."
The Conductor will set classification, skip flags, and hand off to subsequent roles.