* feat(design-system): Liquid Glass redesign portfolio — foundation + primitive kit + Layout shell Operator-requested epic to migrate the UI from the current "warm panel + side-highlight + heavy gradient" visual language to a Liquid Glass aesthetic that retains Deck Hearth's fireplace warmth as accent / gradient / motion (not as panel fill). This squash carries the full 8-convoy portfolio drive-through; 5 sub-convoys reach merged state, 3 land architecture-only and queue impl for follow-up turns gated on dedicated visual-diff baseline re-seeds. Sub-convoy #1 (liquid-glass-design-tokens) — MERGED. 29 CSS custom properties: glass-surface {low,mid,high} alpha ramp + blur/saturate + rim-light (inner/outer) + ember-rim (subtle/pronounced; RGB triple) + 3-tier elevation + modal-scrim, both light + dark themes with eye-perception-corrected alphas; @supports not (backdrop-filter) fallback collapsing surfaces toward solid (preserves ramp ordering). Authored docs/DESIGN_TOKENS.md (270 LOC reference with WCAG AA contrast tables, composite recipes, when-NOT-to-use-glass guidance, per-card grid GPU budget). AGENTS.md gains a § Visual language section as the new agent-contract surface. Sub-convoy #2 (liquid-glass-modal-and-surface-primitive) — Brief 1 MERGED. Adds <GlassSurface> (forwardRef composable; tint / rim / elevation / blur props) and <Modal> primitive (focus-trap, ESC + backdrop close, body-scroll lock, ARIA dialog shape, built-in close button) consuming the token surface. lib/use-focus-trap.js — homegrown hook (~60 LOC, no dep). 10 new vitest cases covering open/close render, ARIA, ESC + closeOnEsc gate, backdrop gate, hideCloseButton, body-scroll lock + restore. 4 reference modal migrations as proof-of-pattern: ShareModal, CollectionDeleteModal, CollectionsCreateModal, CardDetailQuantityModal. Brief 2 (11 remaining modals) queued; CI grandfather list locks the pattern in. Sub-convoy #3 (liquid-glass-form-primitives) — Brief 1 MERGED. Adds <Button> (primary ember-gradient with ember-rim-pronounced; secondary glass-mid; danger; ghost), <Input> (glass-high with ember focus ring + label + helperText + error + aria-invalid + describedby wiring + leadingIcon decorative + trailingAction interactive), <SearchBar> (composes Input with leading search icon + conditional clear button). 10 new vitest cases. pages/login.js + pages/signup.js fully migrated — 2 submit buttons + 7 inputs total; existing test/pages/login.test.js assertion ("Sign in to Deck Hearth" button text) preserved. Brief 2 (profile/settings + deck-builder + scanner + card-editor + collection-cluster modal forms) queued. Sub-convoy #4 (liquid-glass-layout-shell) — MERGED. 6 shell surfaces glass-migrated: desktop sidebar rail (glass-mid + rim + ambient elevation), mobile drawer (glass-mid + pronounced elevation), mobile overlay scrim (modal-scrim + blur-high — visually consistent with <Modal>), search header strip (glass-mid + rim), UserProfileDropdown popover (glass-high + ember-rim-subtle + ambient — matches popover recipe), MobileNavigation bottom bar (replaces legacy mobile-nav-backdrop class). The 5 Layout regression-lock tests (logged-out CTA, no maintainer-email default, "Sign in" link present, supplied email renders, no "Guest" placeholder) all still pass — every edit preserved the documented contract. Sub-convoy #5 (liquid-glass-card-surfaces) — ARCHITECTURE RATIFIED; implementation queued. Pixel-sensitive (rarity-glow reconciliation) so wants a dedicated visual-diff baseline re-seed PR. Pre-blocked on a fix-card3d-state convoy (Card3D has pre-existing state-management bug: state setters used without useState declarations). Sub-convoy #6 (liquid-glass-public-and-auth) — ARCHITECTURE RATIFIED; partial impl shipped via #3 (login + signup form primitives migrated). Landing page editorial + public collection/deck views + login/signup outer-wrapper sweep queued. Sub-convoy #7 (motion-system-pass) — MERGED. 8 motion tokens (5-tier duration taxonomy: instant/quick/default/slow/deliberate; 3 easings: ease-out default, spring for delight, linear for progress) added to the token surface. prefers-reduced-motion upgraded from a narrow nav-item rule to a site-wide universal sweep collapsing animation-duration + transition-duration to 0.01ms (preserves end states, no flicker); .motion-essential class is the opt-in escape hatch for state-meaningful animation (loading spinners, scan reticles). Authored docs/MOTION_SYSTEM.md with WCAG SC 2.3.3 contract, composition recipes, audit of existing keyframes, and adding-new-animation checklist. Sub-convoy #8 (cleanup-legacy-design-css) — Brief 1 MERGED. Two new CI jobs in .github/workflows/ci.yml: (1) forbidden-modal-shell-without-primitive (BLOCKING) — fails build if any new file outside the 9 grandfathered legacy modals uses the fixed inset-0 bg-black bg-opacity- shell pattern; locks in the discipline that every modal must compose <Modal> from components/ui. (2) forbidden-deprecated-color-aliases (WARN-only) — audits pre-Deck-Hearth blue/purple/pink aliases (gradient-text-purple/pink/blue, glow-purple/pink/blue, gradient-bg-purple/blue/pink) as a baseline; graduates to FAIL after #8 Brief 2 sweeps consumers. .cursor/rules/ui-and-theming.mdc updated to document the components/ui/ primitive kit and point at the new canonical reference modals. Verification: lint 0 errors (2 pre-existing warnings in unrelated CardEditorForm.js + CollectionsPageView.js — out of scope); vitest 104/104 passing (was 84 — +20 from new primitive tests: 10 Modal + 10 ui-primitives); ci.yml valid YAML; both new CI gates locally exercised and pass on the current tree. Operator follow-ups documented in .convoys/ship-readiness.md § "Design-system redesign portfolio": - Re-seed Linux visual-diff baselines via Docker workflow (AGENTS.md § 6) after this merges. - preview-smoke.yml runs against the preview; auth + scanner specs touch the migrated surfaces. - Vercel promote to production once smoke + visual gates pass. - Queued follow-up implementer turns: #2 Brief 2 (11 modals), #3 Brief 2 (other forms), #5 Brief 1 (cards, after fix-card3d-state), #6 Brief 1 (landing editorial), #8 Brief 2 (legacy CSS deletion + WARN→FAIL graduation). The user-visible promise — "modern fireplace aesthetic; modals blur the page behind them; reusable components" — is delivered TODAY by the merged work. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(use-focus-trap): preserve named useFocusTrap export for ScannerPageView The portfolio squash inadvertently overwrote the pre-existing lib/use-focus-trap.js (named `export function useFocusTrap(active)` returning a ref — used by ScannerPageView, line 21) with a default- only export shaped for the new `<Modal>` primitive. Vercel build failed: "Export useFocusTrap doesn't exist in target module". Fix: the file now exports BOTH — - `useFocusTrap(active)` (named, original) — returns a ref; pre-Liquid-Glass call sites (ScannerPageView) keep working. - `useFocusTrapContainer({ active, containerRef, ... })` (default, new) — takes a caller-owned ref so panel refs can forward through forwardRef chains (Modal.js consumes this shape). Both hooks are commented to document which to use when. Modal.js imports default already, so no change needed there. Verified: npm run build passes (was failing in CI); lint 0 errors; vitest 104/104 still green. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
103 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.
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.