Doc-drift fix discovered after the fix-layout-default-user cleanup
pass. The bump-next-js convoy shipped on 2026-05-23 as commit
e57ea17, and the commit body explicitly states "Closes P0
ship-blocker #8" — but no dedicated doc-writer pass ever ran for
that convoy, so .convoys/ship-readiness.md still had P0 #8 listed
as open even though Vercel has been deploying main + every PR
successfully since.
Patches only P0 #8 to RESOLVED with the same shape used by P0
#1, #2, #3, #4, #7:
- Title: "— **RESOLVED 2026-05-23**"
- Resolved by: bullet citing e57ea17
- As-shipped: 7-point summary of the bump's actual deliverables
(next 15.4.3->16.2.6, ESLint v9-fallback, typescript devDep,
eslint.config.mjs, scripts.lint update, images.remotePatterns
migration, build verification)
- Side-effects deferred: bump-react, bump-eslint-10, bump-typescript-6,
fix-lint-baseline, App Router migration
- Doc drift note acknowledging this entry was added ~24h late
No code changes. No package.json / lib/** / pages/** / components/**
touched. AGENTS.md was already current (Gotchas #9, #10, #11 already
reflect Next 16 + ESLint v9 + Turbopack post-bump state).
Co-authored-by: Cursor <cursoragent@cursor.com>
Reflects the merged fix-layout-default-user convoy (PR #15) and the
companion CI permissions fix (PR #16) in repo documentation. Also
queues the new fix-vercel-deployment-protection-in-ci convoy that
PR #16 exposed.
.convoys/ship-readiness.md:
- P0 #7: mark RESOLVED 2026-05-24 with squash commit ca302a8.
Document the as-shipped Layout default-null change, the 7-page
sweep, the 5 new regression-lock vitest assertions, and the
queued follow-ups (single-auth-provider, MobileNavigation
cleanup) that stayed explicitly out of scope.
- Queued convoys: add fix-vercel-deployment-protection-in-ci
(P2, CI infra) — PR #16's permissions fix exposed that Vercel
Deployment Protection 401s anonymous CI requests; needs a
bypass-secret plumb to land cleanly. New section also captures
other in-flight follow-ups (rotate-default-admin, cors-tighten,
add-rate-limiting, purge-weak-creds-from-helpers,
single-auth-provider, cleanup-mobile-nav-dead-props,
bump-eslint-10) so the audit trail is centralized.
AGENTS.md:
- § 4 Gotcha #8: mark RESOLVED with commit ref ca302a8. Mirror
the convention used by the prior cleanup commits for #2, #3,
#4, #5 (entry kept, not renumbered).
- § 3 Key conventions: add a new "Layout user prop" bullet
documenting the new default-null + logged-out-CTA contract so
the convention is discoverable from the conventions list, not
just the resolved-gotcha entry.
.convoys/fix-vercel-deployment-protection-in-ci.md (new):
- Queued scaffold. Operator must seed
VERCEL_AUTOMATION_BYPASS_SECRET as a repo secret before the
implementer can run. Decisions to ratify (query param vs.
header), known constraints, acceptance criteria, and
out-of-scope all enumerated.
No changes to: package.json, lib/**, pages/**, components/**,
scripts/**, .github/**, README.md, .cursor/rules/**.
Co-authored-by: Cursor <cursoragent@cursor.com>
* convoy: scope fix-layout-default-user (P0 #7 — Layout maintainer-email leak)
The last remaining P0 ship-blocker from .convoys/ship-readiness.md.
components/Layout.js line 562 defaults the user prop to a real email
address (me@randallstillwell.com); any page that renders Layout without
passing user explicitly impersonates the maintainer.
Scope: components/Layout.js + audit of 17 pages that import Layout
(grep-confirmed list in convoy file). Single PR likely. Auditor cohort
skipped (no design-system, IA, or browser-smoke surface).
Architect to address:
- Q1: logged-out rendering branch design (navbar, mobile-nav,
auth-only items treatment)
- Q2: page audit triage into always-auth / public-or-auth /
anonymous-allowed buckets
- Q3: brief decomposition (single brief / 2 briefs in 1 PR / fan-out)
- Q4: whether to add vitest coverage for the logged-out branch
(recommend yes — small surface, high regression protection)
Hard out-of-scope: branding (pick-a-name), auth-provider collapse
(single-auth-provider), Layout god-component split (god-component-split).
depends_on: bump-next-js (shipped), fix-auth-bypass (shipped),
drop-public-setup (shipped)
addresses: P0 #7 from .convoys/ship-readiness.md
parent: ship-readiness
Co-authored-by: Cursor <cursoragent@cursor.com>
* architect(fix-layout-default-user): plan + briefs 1-2 (Layout fix + page audit)
2 briefs, single PR. ~12 files net (down from the 18 in the original scope —
10 of the 17 Layout-importing pages already pass user explicitly).
Brief 1: components/Layout.js default user=null + Sign-in CTA branch in
UserProfileDropdown when logged out. Adds first jsdom test in the repo
at test/components/Layout.test.js (Decision D2) with 5 regression-lock
assertions. devDeps: jsdom@^29, @testing-library/react@^16.
Brief 2: page audit sweep — 7 pages need code changes:
- Pass user={user} to Layout: scanner.js, deck-builder.js (×4),
deck/[id].js (×3), decks.js (×3)
- Replace page-level useState({email: 'me@...'}) → useState(null) +
null-guards: profile.js, settings.js
- Replace hardcoded const user = {email: 'me@...'} with useAuth():
card/[id].js
Discovered second anti-pattern: profile.js, settings.js, card/[id].js
seed page-level state with the maintainer email. Folded into Brief 2 since
success metric "no real email address remains in any component default-prop"
reads naturally to include page-level seed values.
Decisions:
A1 — Sign-in CTA replaces avatar+email+dropdown when user===null;
hides auth-only dropdown (Profile/Settings/Logout/Admin);
keeps public + community nav visible
B — Per-page bucket assignment (10 already correct, 7 need fix);
full per-page table with justification in convoy file
C2 — Two briefs in one PR (Brief 1 = Layout + test; Brief 2 = page
sweep depends on Brief 1). C1 buries the conceptual change under
mechanical edits; C3 is over-orchestrated for this scope
D2 — vitest lock-in; first jsdom test in repo; same negative-regression
style as test/lib/permission-middleware.test.js (synthetic-admin
shape). devDeps jsdom + @testing-library/react
Risks tracked R1-R8. Biggest: R2 (useState(null) null-deref in 3 leaky
pages — mitigated by audit-pass mandate + manual smoke).
MobileNavigation deliberately NOT folded in: its user prop is dead code
(never reads user.*); different bug class; cleanup queued separately to
avoid scope expansion.
Flagged-but-deferred:
- 4 pages still import useAuth from lib/auth-context.js
→ single-auth-provider (queued P1 #9)
- Layout headers still render "Deck Hearth" / "DH" branding
→ pick-a-name (queued P1 #12)
- MobileNavigation dead user prop → cleanup-mobile-nav-dead-props
or fold into god-component-split
addresses: P0 #7 from .convoys/ship-readiness.md (last P0 ship-blocker)
parent: ship-readiness
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(layout): default user=null + Sign-in CTA when logged out (Brief 1 of fix-layout-default-user)
Closes the source-side half of P0 #7 from .convoys/ship-readiness.md.
The page-side sweep (Brief 2) follows in a separate commit.
components/Layout.js:
- Default user prop is now null (was hardcoded to
{ email: 'me@randallstillwell.com', role: 'user' })
- UserProfileDropdown renders a "Sign in" link to /login when
user === null instead of the maintainer's email + auth-only menu
items (Decision A1)
- All user.* accesses guarded with optional chaining or null checks
- useState hook stays above the new null-user early return to satisfy
rules-of-hooks (boot-the-brief caught this on the first try;
see AGENTS.md Gotcha #11.5)
test/components/Layout.test.js (new):
- First jsdom test in the repo (Decision D2)
- 5 regression-lock assertions: no maintainer email ever rendered
(prop omitted, prop=null), Sign-in link exists with href=/login,
supplied email renders when prop is set, no "Guest" placeholder
(locks A1 copy choice)
- Mocks next/link, next/router (prefetch, replace, events, query),
and theme-context.useTheme for jsdom safety under Next 16
package.json + package-lock.json:
- Add jsdom@^29 and @testing-library/react@^16 to devDependencies
- @testing-library/dom@^10 added explicitly (peer auto-install
skipped it under npm 11; brief anticipated this fallback)
vitest.config.js (deviation from brief — see PR description):
- Add esbuild { loader: 'jsx', jsx: 'automatic' } so vitest can
parse JSX in .js files. Required to import any React component
written in the repo's Next.js pages-router .js convention
(AGENTS.md Gotcha #9). The brief said "no change" to this file,
but JSX-in-.js parsing is a hard prerequisite for the new test
to import components/Layout.js — the alternatives (rename test
to .test.jsx; rewrite test in React.createElement) either break
the test glob or still hit the same Layout.js parse failure.
Other tests are unaffected (they import non-JSX modules).
Smoke output: see PR description.
addresses: P0 #7 from .convoys/ship-readiness.md (last P0 ship-blocker)
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(pages): pass user explicitly + null-guard leaky page seeds (Brief 2 of fix-layout-default-user)
Closes the page-side half of P0 #7 from .convoys/ship-readiness.md.
Brief 1 (commit ddf8fd2) handled the Layout-side fix.
Per the architect's per-page bucket table (Decision B in
.convoys/fix-layout-default-user.md), 7 pages needed code changes;
the other 10 of 17 Layout-importing pages already pass `user` correctly.
Pass user={user} to Layout (4 pages, 11 call sites):
- pages/scanner.js (1 call)
- pages/decks.js (3 calls)
- pages/deck-builder.js (4 calls)
- pages/deck/[id].js (3 calls)
(All four still import useAuth from lib/auth-context.js — that's
intentional and stays as-is until the single-auth-provider convoy
collapses the three parallel auth surfaces.)
Replace leaky page-level seed values with useState(null) + null guards
(2 pages, R2 mitigation):
- pages/profile.js: useState({email: 'me@...', role: 'user', ...})
→ useState(null) + ?. on every sync user.* read
+ early-return guards in getDisplayName/getInitials
+ conditional render around the "Member since" block
so formatDate(undefined) never runs
- pages/settings.js: same pattern (single user.email reader guarded)
Replace hardcoded const with useAuth from lib/use-auth.js (1 page):
- pages/card/[id].js: const user = {email: 'me@...'}
→ const { user } = useAuth() (called unconditionally
at the top of the component; rules-of-hooks safe)
Verification:
- grep 'me@randallstillwell.com' pages/ → 0 hits
- 21/21 vitest tests pass (16 pre-existing + 5 from Brief 1)
- npm run lint matches baseline (128 problems pre, 128 post; verified
via git stash before/after)
- Manual static read-through of every diff; ReadLints clean on the 7
files
- Dev-server smoke: /cards anonymous returned HTTP 200 with 0
'me@randallstillwell' matches before the user's shared dev server
became unresponsive mid-session (same dev-server-shared-by-user
constraint flagged in Brief 1); interactive logged-in smoke is
parent/operator gated
Flagged-but-deferred (untouched per scope):
- 4 pages still import useAuth from lib/auth-context.js
→ single-auth-provider (queued P1 #9)
- components/MobileNavigation.js still receives dead user prop
→ cleanup-mobile-nav-dead-props (or fold into god-component-split)
addresses: P0 #7 from .convoys/ship-readiness.md (last P0 ship-blocker)
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Reflects the merged drop-public-setup convoy (PR #13) in repo
documentation. Small focused pass; no behavior changes.
AGENTS.md:
- § 1 auth bullet: replace "seed admin row still ships in
setup-neon-db.js" claim with the new env-var-gated reality and
the R1 operator-rotation caveat.
- § 4 Gotcha #4: mark RESOLVED with commit refs (ff80753 + b63b509),
document the as-shipped behavior, the Brief 2 CJS→ESM Node 22.x
fix, and the R1 operator caveat. Entry kept (not renumbered) per
the same convention used for resolved gotchas #2, #3, #5.
- § 5 Running locally: add ADMIN_INITIAL_PASSWORD to the env-var
template list with a note that setup-db exits 1 if it's unset.
.convoys/ship-readiness.md:
- P0 #3: mark RESOLVED 2026-05-23 with commit refs, document the
full as-shipped behavior including Brief 2's CJS→ESM bonus,
the R1 operator caveat (Decision A — going-forward only),
and the deferred sibling weak-cred references queued for
purge-weak-creds-from-helpers.
.cursor/rules/no-go-zones.mdc:
- Editing rules of thumb: clarify the schema-vs-operational
distinction for scripts/setup-neon-db.js. drop-public-setup
set the precedent that operational changes (env-var gating,
pre-flight validation, module-system fixes) are allowed in
place, while DDL changes still need a separate migration
script. Documented so future agents don't have the same
confusion the drop-public-setup architect did (see Decision B
in .convoys/drop-public-setup.md).
No changes to: package.json, lib/**, pages/**, components/**,
scripts/**, .github/**, README.md (already updated in PR #13).
Co-authored-by: Cursor <cursoragent@cursor.com>
Mid-convoy discovery: brief 1's implementer confirmed scripts/setup-neon-db.js
does not actually run on Node 22.x because bump-next-js added "type": "module"
to package.json but the seed script still uses CJS require() calls.
Throws ReferenceError immediately on `npm run setup-db`.
Architect's original "Anything flagged but not acted on" #1 claim that "it
runs successfully today under Node 22" was incorrect for Node 22.14.0.
Decision D (ratified by user 2026-05-23): expand convoy to include brief 2
rather than queue a separate convert-setup-db-to-esm follow-up. Rationale:
brief 1's env-var gate is theatrical security on a script no operator can
execute; the CJS→ESM conversion is mechanical (~6 LOC, same file, no
functional changes); splitting into two convoys creates a regression window
where operators on Node 22.x cannot bootstrap a database.
Brief 2 scope: pure module-system conversion in scripts/setup-neon-db.js:
- require('dotenv').config(...) → import dotenv + dotenv.config(...)
- require('@neondatabase/serverless') → import { neon }
- inline require('bcryptjs') hoisted to top-of-file import
- no functional changes; same DDL, same env-var gate, same console.logs
Verification: smoke must now show npm run setup-db actually executes (no
ReferenceError); brief-1 env-var gate must still fire as documented;
all 16 vitest tests must still pass.
Updated:
- .convoys/drop-public-setup.md Decomposition (brief 2 added, depends_on brief 1)
- .convoys/drop-public-setup.md slice_dependencies YAML
- .convoys/drop-public-setup.md § Decisions (added Decision D)
- .convoys/drop-public-setup.md § Anything flagged but not acted on
(item #1 marked resolved by brief 2)
addresses: P0 #3 from .convoys/ship-readiness.md + Node 22.x compat
parent: ship-readiness
Co-authored-by: Cursor <cursoragent@cursor.com>
Single-brief convoy. ~25 LOC net across 2 files (scripts/setup-neon-db.js,
README.md). No fan-out; single PR.
Decisions:
A1 — going-forward only (matches JWT_SECRET pattern from
fix-auth-bypass Brief 1). Existing weak-hash admin rows in
deployed envs are NOT rotated; operators rotate manually
via the app after merge. Queue rotate-default-admin follow-up
if a real audit finds a deploy still on the weak hash.
B — operational change to setup-neon-db.js is allowed; no-go-zones
rule prohibits SCHEMA edits, not env-var gating.
C1 — no vitest coverage. Fail-loud path is validated by manual smoke
(the brief mandates pasting fail-loud + happy-path output into
the PR description).
Key risks tracked: R1 (existing weak hash), R2 (unhelpful error),
R3 (stdout password leak — delete the console.log lines, do NOT
interpolate the env-var), R4 (silent rotation if ON CONFLICT changed
to DO UPDATE), R5 (README env block omits the new var), R6 (3 sibling
files still have admin123 — out of scope per convoy spec).
Flagged-but-deferred:
- CommonJS in ESM package (setup-neon-db.js) → convert-setup-db-to-esm
- admin123 in reset-db.js, create-test-users.js, TESTING_GUIDE.md
→ purge-weak-creds-from-helpers (or fold into launch-polish)
- admin@tcgvault.com hardcoded email → pick-a-name convoy
addresses: P0 #3 from .convoys/ship-readiness.md
parent: ship-readiness
Co-authored-by: Cursor <cursoragent@cursor.com>
Closes P0 #3 from .convoys/ship-readiness.md. Removes the hardcoded
admin@tcgvault.com / admin123 credentials from scripts/setup-neon-db.js
(make it env-var-driven) and from README.md.
Single PR; small surface (2 files). No UI, no API, no migration. Auditor
cohort skipped (no UX/a11y/design-system surface).
P0 #3's third file (pages/api/setup-database.js) was already deleted by
fix-auth-bypass Brief 3.
Architect to address:
- Existing-admin rotation story (going-forward fix vs. forced reset)
- Confirm scripts/setup-neon-db.js no-go-zones rule applies to schema
changes only (this is operational)
- Whether to add vitest coverage for the env-var-required path
depends_on: bump-next-js (shipped), fix-auth-bypass (shipped)
Co-authored-by: Cursor <cursoragent@cursor.com>
Conductor output for the highest-priority convoy in the launch
sequence. Closes P0 ship-blocker #8 from .convoys/ship-readiness.md.
Vercel is currently refusing to deploy any branch (including main)
due to a CVE in next@15.4.3 ("Vulnerable version of Next.js
detected"). Last successful main deploy: 2025-08-01. Until this
convoy lands, every downstream preview-smoke / visual-diff gate is
non-functional.
Classification: feature
Skip: ia, ux, flag
Next role: role-architect
Routing straight to architect (IA + UX skipped — no information
architecture or UX change). Architect reads the Next 15 → 16
migration guide and produces 1–3 briefs covering the bump itself,
any required code migrations (likely next.config.js
images.domains → images.remotePatterns), and Vercel preview
verification.
Audit cohort (post-PR draft, /multitask group):
reviewer + design-system-auditor + a11y-auditor
Out of scope here (own convoys):
- React 18 → 19 bump → bump-react (if/when desired)
- App Router migration → out of horizon
- @playwright/test install → adopt-playwright-smoke
- ESLint baseline cleanup → fix-lint-baseline
Convoy file: .convoys/bump-next-js.md
Analytics: emitted via scripts/log-convoy-event.sh
Co-authored-by: Cursor <cursoragent@cursor.com>
The throwaway bootstrap PR exposed three pre-existing issues that
weren't visible before the pipeline was installed:
1. ESLint had no config (`.eslintrc.json` missing) even though the
`lint` script and deps were both present. `next lint` was prompting
interactively in CI. Added `.eslintrc.json` extending
`next/core-web-vitals` (Next.js Strict).
2. Running lint surfaced ~100 pre-existing errors, including several
real bugs (conditional React hook calls in components/pages).
Marked the CI lint job `continue-on-error: true` with an explicit
TODO so PRs aren't blocked while a follow-up convoy
(fix-lint-baseline) cleans up the codebase. Lint output is still
visible in PR logs.
3. Vercel is platform-blocking every deployment with "Vulnerable
version of Next.js detected" — locked at 15.4.3, latest is 16.2.6.
The last successful Vercel deploy on main was 2025-08-01. Until
Next.js is bumped, every preview-smoke / visual-diff gate is
non-functional. Added as P0 #8 with a new `bump-next-js` convoy at
the front of the launch sequence.
Updated `.convoys/ship-readiness.md`:
- P0 #8: Vercel deploy blocked by Next.js CVE
- P1 #11.5: pre-existing lint baseline
- Launch sequence: prepend `bump-next-js` at step 0, add
`fix-lint-baseline` at step 3.5
Co-authored-by: Cursor <cursoragent@cursor.com>