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>
15 KiB
| convoy | brief_number | depends_on | files | cross_brief_commitments | |||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| fix-layout-default-user | 2 |
|
|
|
Brief 2: Pages — pass user explicitly to Layout, and drop page-level maintainer-email defaults
Goal (1 sentence)
Audit the 17 pages that render Layout and fix the seven that today either omit the user prop (so Layout's default kicks in) or seed user from a hardcoded email: 'me@randallstillwell.com' initializer; after this brief, every page sources user from an auth hook (or passes user={null} deliberately on anonymous-allowed surfaces).
Files in scope (do not edit anything else)
pages/scanner.js— modified.pages/deck-builder.js— modified.pages/deck/[id].js— modified.pages/decks.js— modified.pages/profile.js— modified.pages/settings.js— modified.pages/card/[id].js— modified.
Conventions to follow
.cursor/rules/auth-and-permissions.mdc§ Canonical surface vs Legacy. The canonical client hook islib/use-auth.js::useAuth. The legacylib/auth-context.js::useAuthandlib/admin-auth.js::useAdminare still wired for compatibility but should not gain new consumers. However, four of the seven files in this brief already importuseAuthfromlib/auth-context.js. Do NOT migrate those imports tolib/use-auth.jshere — that's the queuedsingle-auth-providerconvoy's job. The migration touches ~30 files and needs a coordinated sweep; cherry-picking four of them would diverge that convoy's plan. Specifically:pages/scanner.js— keepimport { useAuth } from '../lib/auth-context';.pages/deck-builder.js— keepimport { useAuth } from '../lib/auth-context';.pages/deck/[id].js— keepimport { useAuth } from '../../lib/auth-context';.pages/decks.js— keepimport { useAuth } from '../lib/auth-context';.
.cursor/rules/ui-and-theming.mdc§ Component conventions. Pages that render Layout MUST passuserexplicitly. Page-leveluseState({ email: 'me@…' })initializers fail the same rule even though Layout itself is fixed; clear them..cursor/rules/no-go-zones.mdc. Do not editcomponents/Layout.js.backup.- Brief size discipline. This brief is seven page-level edits, each small. Do NOT:
- Touch any file outside
files:above. Specifically: nocomponents/**, nolib/**, notest/**, nopackage.json, no.github/**, no.cursor/rules/**. - Add new vitest tests. Brief 1's
test/components/Layout.test.jscovers the contract at the component boundary; per-page tests would duplicate it. - Migrate a page off
lib/auth-context.js(see above). - Restructure
loading/useEffectchains. Each page already has a working data-fetch pattern; the only thing changing is where the seedusercomes from. - Convert pages to TypeScript (the repo is JavaScript-only —
AGENTS.mdGotcha #9).
- Touch any file outside
- No new dependencies. All seven files use modules already imported elsewhere in the codebase.
Acceptance criteria
The seven files split into two patterns. Apply the corresponding pattern to each.
Pattern A — pages that omit user on Layout (4 files)
These pages already call useAuth() (from lib/auth-context.js) but render <Layout> without forwarding the user reference. Add user={user} to every <Layout> call.
pages/scanner.js
- Locate line 333:
<Layout>. - Replace with
<Layout user={user}>. - No other change to the file. The existing
import { useAuth } from '../lib/auth-context';(line 8) andconst { user } = useAuth();(line 11) stay as-is. - Verify the
if (!user) router.push('/login')redirect (line 30-35) still runs first. After Brief 1, Layout will render the logged-out shape briefly whileuser === nullandloading === true; once the redirect to/loginfires, the user lands on the AuthLayout-rendered login page. Acceptable.
pages/deck-builder.js
- Replace each of the four
<Layout>calls with<Layout user={user}>. Lines (per current grep): 262, 277, 287, 303. - No other change.
import { useAuth } from '../lib/auth-context';(line 7) andconst { user } = useAuth();(line 11) stay.
pages/deck/[id].js
- Replace each of the three
<Layout>calls with<Layout user={user}>. Lines: 132, 142, 160. - No other change.
import { useAuth } from '../../lib/auth-context';(line 6) andconst { user } = useAuth();(line 10) stay.
pages/decks.js
- Replace each of the three
<Layout>calls with<Layout user={user}>. Lines: 161, 176, 185. - No other change.
import { useAuth } from '../lib/auth-context';(line 5) andconst { user } = useAuth();(line 8) stay.
Pattern B — pages with hardcoded maintainer-email initializers (3 files)
These pages seed their user state/const with the maintainer's email at the page level. Replace the initializer with null (and source from useAuth() where the page does not already have an auth hook). Add the necessary null-guards on sync reads.
pages/profile.js
- Replace lines 10-20 (
const [user, setUser] = useState({ email: 'me@randallstillwell.com', role: 'admin', first_name: '', last_name: '', username: '', bio: '', avatar_url: '', favorite_games: ['MTG'], created_at: new Date().toISOString() });) with:
const [user, setUser] = useState(null);
- Add null-guards on every sync read of
user.*in the JSX. The reader functions (getDisplayName,getInitialsnear lines 239-249) already use optional chaining where it matters (user.first_name,user.last_name,user.username,user.email); confirm they handlenull:getDisplayName(): returnsuser.first_name || user.last_name ...— change to start withif (!user) return '';so the first sync render is safe.getInitials(): same pattern —if (!user) return '';at the top.- In the JSX (around line 260-450), every
user.email,user.role,user.username,user.bio,user.avatar_url,user.first_name,user.last_name,user.created_ataccess must useuser?.*(optional chaining). Most are inside{user && (...)}blocks already; verify each one.
- The
loadingbranch (line 258-267,if (loading) { return <Layout user={user}>... }) renders<Layout user={user}>— after this brief,userisnullduring loading, so Layout will show the "Sign in" branch. That's correct;loadUserProfileredirects to/loginif no token (line 51-54), so this flash only happens for already-authenticated users while their profile fetches. - Verify with
rg "user\." pages/profile.js— every match must be inside an optional-chain (user?.) or a{user && ...}guard orif (!user)early-return. - Do NOT add an
import { useAuth } from '...'. The page's existing pattern (manual fetch from/api/user/profile) stays — addinguseAuthhere would be a second source of truth and asingle-auth-providermigration. The point of this brief is that the seed value isnull, not who provides it.
pages/settings.js
- Replace lines 9-12 (
const [user, setUser] = useState({ email: 'me@randallstillwell.com', role: 'admin' });) with:
const [user, setUser] = useState(null);
- Audit every
user.*read in the JSX (notably line 306value={user.email}in the email read-only field). Wrap each in optional chaining or a{user && (...)}guard. The simplest fix for line 306 isvalue={user?.email || ''}. - The
loadingbranch (line 228-236,if (loading) { return <Layout user={user}>... }) renders<Layout user={user}>— same flow asprofile.js. After this brief, the loading flash shows "Sign in" briefly untilloadSettingsredirects (line 53-55) or resolves with the real user. - Do NOT add
useAuth. Same reasoning asprofile.js. - Verify with
rg "user\." pages/settings.js— every match must be optional-chain / guard / early-return safe.
pages/card/[id].js
- Replace lines 13-16 (the hardcoded
const user = { email: 'me@randallstillwell.com', role: 'user' };) with:
const { user } = useAuth();
- Add the import at line 4 (between the existing
useIsAdminimport on line 4 and the next line — alphabetical / grouping is the implementer's call):
import { useAuth } from '../../lib/use-auth';
lib/use-auth.jsis the canonical hook (auth-and-permissions.mdc§ Canonical surface).card/[id].jsdoes not currently import any auth hook (theuseIsAdminimport is fromlib/admin-auth.js, butuseIsAdminreturns a boolean, not a user object), so we are adding a fresh consumer rather than migrating one — the "no new consumers of legacy providers" rule is satisfied.- Audit every
user.*read in the file —pages/card/[id].jsis 913 lines; runrg "user\." pages/card/[id].jsand confirm every match isuser?.*or under a{user && (...)}guard. Likely matches:user.userId,user.email,user.role. The page is public-or-authenticated, so eachuser.*site should already gracefully handle "no user" — the convoy's bucket assignment for this page is Bucket 2 (public-or-auth). - The three
<Layout user={user}>calls (lines 343, 353, 376) stay —useris now sourced fromuseAuth()and may benullfor anonymous viewers. - Verify the page still renders for an anonymous visitor at
/card/some-idafter the change. (Smoke test below.)
Repo-wide grep verification (run before opening PR)
rg "me@randallstillwell" --type jsreturns hits ONLY incomponents/Layout.js.backup(no-go zone, untouched). Specifically: NO hits incomponents/Layout.js,pages/profile.js,pages/settings.js,pages/card/[id].js. (The existingcomponents/Layout.jshit was removed by Brief 1.)rg "<Layout>" pages --type jsreturns ZERO hits — everyLayoutopening tag inpages/includes auser=prop.rg "import.*useAuth.*lib/auth-context" pages --type jsreturns the SAME four hits as before this brief (pages/scanner.js,pages/deck-builder.js,pages/deck/[id].js,pages/decks.js). No new hits, no removals — confirms we didn't migrate the legacy-provider consumers.rg "import.*useAuth.*lib/use-auth" pages --type jsincludespages/card/[id].js(new hit) plus the existing six (pages/dashboard.js,pages/my-cards.js,pages/cards.js,pages/collection/[identifier].js,pages/community/collections.js,pages/collections.js).
Smoke (manual)
Run these in order; paste the relevant output / screenshots into the PR description. Each scenario MUST be tested both logged out (no auth_token in localStorage) and logged in (a real user account).
/scanner- Logged out: redirected to
/login. Layout never renders maintainer email (the redirect fires fromuseEffectbut Layout briefly shows "Sign in" before navigation). DevTools console: no errors. - Logged in: navbar shows real user email; scanner UI loads.
- Logged out: redirected to
/deck-builder(no query string, just the bare route)- Logged out: Layout shows "Sign in" in the sidebar; the page's own loading / empty state renders without crashing.
- Logged in: navbar shows real user email; deck builder loads.
/deck/<some-id>(use any deck id fromnpm run devadmin → decks list)- Logged out (public deck): Layout shows "Sign in"; the deck detail still renders (this is a Bucket 2 page).
- Logged in: navbar shows real user email; deck detail loads.
/decks- Logged out: Layout shows "Sign in"; the page's own auth-gated content (private deck list) shows the appropriate empty / login-prompt state.
- Logged in: navbar shows real user email; deck list loads.
/profile- Logged out: redirected to
/loginbyloadUserProfile's 401 branch. Layout briefly shows "Sign in" before redirect. Nome@randallstillwell.comflash anywhere — this is the change you are smoke-testing. - Logged in: navbar shows real user email; profile loads with real user data.
- Logged out: redirected to
/settings- Logged out: redirected to
/login. Same flash as profile. - Logged in: navbar shows real user email; settings load.
- Logged out: redirected to
/card/<some-id>(use any card id from/cards)- Logged out: Layout shows "Sign in"; card detail still renders (this is the public-card-detail page, Bucket 2). No
me@randallstillwell.comanywhere on the page. Open DevTools → Console →document.body.innerText.includes('me@randallstillwell.com')→ expectfalse. - Logged in: navbar shows real user email; card detail loads, "Add to collection" UI works.
- Logged out: Layout shows "Sign in"; card detail still renders (this is the public-card-detail page, Bucket 2). No
- Lint.
npm run lintexits 0 (or matches the existing pre-PR baseline; do not introduce new lint errors). - Tests.
npm run test:runis green: 5 (Brief 1) + 24 (pre-existing) = 29 tests pass.
Out of scope (do not do these)
- No edits to any of the other 10 pages (
pages/dashboard.js,pages/my-cards.js,pages/cards.js,pages/collection/[identifier].js,pages/community/collections.js,pages/collections.js,pages/invite/accept.js,pages/invite/decline.js,pages/admin/card-import.js,pages/admin/card-editor.js). They already passusercorrectly per the per-bucket audit in.convoys/fix-layout-default-user.md§ Decisions B. - No edits to
components/**,lib/**,test/**,package.json,package-lock.json,.github/**,.cursor/rules/**. - No migration of the four legacy-provider importers (
scanner.js,deck-builder.js,deck/[id].js,decks.js) tolib/use-auth.js. That'ssingle-auth-provider. - No removal of the dead
userprop onMobileNavigation— flagged for follow-up. - No vitest tests added — Brief 1's
test/components/Layout.test.jsis the convoy's single test addition. - No
AGENTS.mdGotcha #8 update — post-convoy doc-writer pass. - No README / CHANGELOG / TESTING_GUIDE updates.
Rationale (≤3 sentences)
The convoy's success metric requires that no real email address remains in any component default-prop AND that every Layout call site passes user correctly; Brief 1 fixed Layout but four pages omit the prop entirely (re-triggering the default if not for Brief 1's null default) and three pages seed page-level state with the maintainer email (a parallel anti-pattern that Brief 1 cannot reach). Splitting these seven mechanical edits into Brief 2 keeps Brief 1's diff focused on the actual fix + test, while this brief sweeps the call sites in one pass. Leaving the four legacy-auth-context imports alone preserves the planned single-auth-provider migration's clean diff — fixing the user-prop default does not require re-architecting which auth hook a page imports.