# cleanup-mobile-nav-dead-props (P3 polish — single-prop hygiene)

**Status:** SHIPPED 2026-05-26 (PR TBD)
**Classification:** hygiene
**Priority:** P3 polish (not a bug, not a security issue; dead-prop
removal is purely a clarity-of-surface cleanup)
**Convoy owner:** parent (no architect — single-line prop removal in
one component + one caller; surfaced and pre-decided in a sibling
convoy)
**Opened:** 2026-05-26

## Background

`components/MobileNavigation.js` accepts `{ user, onMenuOpen }` but
never reads `user.*` — the bottom-bar items (Cards, Decks, Dashboard,
Community, More) are static and don't depend on auth state or role.

This was originally surfaced as **R8** in the `fix-layout-default-user`
convoy (see `.convoys/fix-layout-default-user.md` § R8 and § "Anything
flagged but not acted on") and deferred there with explicit
instructions: *"If the implementer is tempted to delete the prop, they
MUST stop — that's god-component-split / single-auth-provider
territory."* The deferral was correct for that convoy's scope; it is
no longer needed because the prop is genuinely dead at the current
static-bar reality, and removing it does not require a wider auth
refactor.

The follow-up was queued as `cleanup-mobile-nav-dead-props` in
`.convoys/ship-readiness.md` § Queued convoys, with a note that it
may fold into `god-component-split` (P2 #13) if that lands first.
God-component-split has not landed; this small hygiene convoy ships
first.

## Audit results

Pre-edit audit (the spec's "don't blindly trust the queue entry"
clause):

1. **Reading `components/MobileNavigation.js`** — the file is 171
   lines. Line 5 destructures `{ user, onMenuOpen }`. Lines 6–170 use
   `onMenuOpen` exactly once (line 39, as the `onClick` for the "More"
   button). `user` does not appear elsewhere — no `user.email`,
   `user.role`, `user.id`, no conditional render gated on `user`, no
   pass-through to a child component. The bottom-bar `navigationItems`
   array is hardcoded and does not branch on auth state.
2. **`rg '\buser\b' components/MobileNavigation.js`** before edit: 1
   hit (the destructure on line 5). After edit: 0 hits.
3. **`rg "MobileNavigation" components/ pages/ --type js`**: two
   import + JSX-callsite pairs in the codebase:
   - `components/Layout.js` (active) — line 5 import, line 598-601 JSX
     call passing `user={user}` and `onMenuOpen={...}`.
   - `components/Layout.js.backup` (no-go-zone per
     `.cursor/rules/no-go-zones.mdc` § "Append-only / historical" —
     "legacy snapshot; delete with a real PR, never edit") — line 5
     import, line 264 JSX call. Left untouched per the no-go-zone
     rule; if/when the `.backup` file is eventually deleted, this dead
     call disappears with it.

Audit verdict: `user` is genuinely dead. Cleanup is safe.

## The fix

Two-file, three-line diff:

1. **`components/MobileNavigation.js` line 5**: remove `user` from the
   destructured props.
   - Before: `export default function MobileNavigation({ user, onMenuOpen }) {`
   - After:  `export default function MobileNavigation({ onMenuOpen }) {`
2. **`components/Layout.js` lines 598-601**: remove the `user={user}`
   JSX attribute from the only active call site.
   - Before:
     ```
     <MobileNavigation
       user={user}
       onMenuOpen={() => setIsMobileMenuOpen(true)}
     />
     ```
   - After:
     ```
     <MobileNavigation
       onMenuOpen={() => setIsMobileMenuOpen(true)}
     />
     ```

No new code. No refactors. No tests added (the component has no
direct test coverage; `test/components/Layout.test.js` tests Layout's
logged-out branch and does not assert on `MobileNavigation`'s prop
shape).

## Out of scope

- The unused `import { useState } from 'react'` on
  `components/MobileNavigation.js` line 3. The hook is imported but
  not called. This is a pre-existing dead import unrelated to the
  `user` prop; the convoy spec explicitly forbids "refactor anything
  else in MobileNavigation.js (this is a single-prop removal)". A
  future hygiene pass can sweep it (or it'll get caught by an
  eventual lint-no-unused-imports rule).
- `components/Layout.js.backup` — no-go-zone, untouched.

## Verification plan

1. `rg '\buser\b' components/MobileNavigation.js` → 0 hits (post-edit
   confirmation that the prop is truly gone, not just renamed).
2. `rg "MobileNavigation" components/ pages/ --type js` → confirm
   each active call site passes only `onMenuOpen`.
3. `npm run lint` → 128 problems baseline preserved (no regression
   introduced; no new dead-code/unused-var warnings created by the
   change).
4. `npm run test:run` → 21/21 pass. Specifically,
   `test/components/Layout.test.js` continues to pass — its
   regression-lock assertions for the logged-out Layout branch
   (Gotcha #8) do not depend on `MobileNavigation`'s prop shape, so
   the dead-prop removal is invisible to that suite.
5. `npm run build` skipped — relying on Vercel preview CI. Trade-off:
   single-prop removal in a leaf component is extremely low risk of
   build-time regression, and the Playwright smoke + visual-diff
   workflows on the PR will catch any Layout-rendering issue before
   merge.

## Risks

- **R1 — A future feature that wants per-user bottom-bar items would
  need to re-add the prop.** Hypothetical examples: showing an
  unread-count badge on a "Notifications" tab gated on `user.id`, or
  hiding the "Community" tab for unauthenticated visitors. **Accepted.**
  Re-adding a prop is a one-line change when the feature actually
  lands; carrying a dead prop "just in case" obscures the current
  surface and adds nothing. The cleanup is correct for the
  current static-bar reality; future features pay their own
  add-the-prop cost.
- **R2 — `components/Layout.js.backup` still references the old prop
  shape.** **Accepted.** The backup is a no-go-zone (per
  `.cursor/rules/no-go-zones.mdc`) and is dead code by definition.
  Touching it would violate the rule; leaving it as a stale snapshot
  is the convention. When the backup is eventually deleted in a
  separate convoy, this stale call disappears with it.

## Acceptance criteria

- 2 files modified, 3 lines net change (1 line edit + 1 attribute
  removal from a multi-line JSX block).
- `npm run lint` exit 1 with 128 problems (baseline preserved).
- `npm run test:run` 21/21 pass.
- `rg '\buser\b' components/MobileNavigation.js` → 0 hits post-edit.

## Owns

Parent (single-prop removal in a leaf component; no architect or
implementer subagent required; audit confirms the queue entry's
premise).

## As-shipped

Single squash commit `171f5af` (PR #28, merged 2026-05-27T03:53:29Z
UTC / local 2026-05-26). Parent-owned end-to-end per the convoy spec
— no architect, no implementer subagent dispatched. Single-prop
removal in a leaf component, exactly as planned; no mid-execution
surprises.

**Diff: 3 files, +156 / -2.** `components/MobileNavigation.js` (1
line edit — removed `user` from the destructured props) +
`components/Layout.js` (1 attribute removal from the JSX call site —
removed `user={user}`) + `.convoys/cleanup-mobile-nav-dead-props.md`
(the planning document, committed atomically with the fix). The +156
addition figure is dominated by the planning doc; actual source diff
is 3 lines net.

**The two surface edits:**

1. **`components/MobileNavigation.js` line 5:** `export default function
   MobileNavigation({ user, onMenuOpen }) {` → `export default function
   MobileNavigation({ onMenuOpen }) {`.
2. **`components/Layout.js` lines 598-601:** removed the `user={user}`
   JSX attribute from the only active `<MobileNavigation>` call site.

**Audit verdict pre-fix confirmed.** The pre-edit grep
(`rg '\buser\b' components/MobileNavigation.js`) returned 1 hit (the
destructure on line 5); post-edit grep returned 0 hits. The bottom-bar
items (Cards, Decks, Dashboard, Community, More) are static and don't
depend on auth state or role — the `user` prop was genuinely dead.

**`components/Layout.js.backup` left untouched** per the
`.cursor/rules/no-go-zones.mdc` § "Append-only / historical" rule
("legacy snapshot; delete with a real PR, never edit"). The backup
still references the old `MobileNavigation({ user, onMenuOpen })`
prop shape; when the `.backup` file is eventually deleted in a
separate convoy, that stale call disappears with it. This is the
documented out-of-scope handling.

**Verification (all gates green at merge):**
- `rg '\buser\b' components/MobileNavigation.js` → 0 hits post-edit
- `rg "MobileNavigation" components/ pages/ --type js` → 1 active
  call site (`components/Layout.js`) passing only `onMenuOpen`, plus
  the historical `.backup` reference (intentional)
- `npm run lint` → 125 problems (post-PR-#31 baseline preserved; no
  new dead-code/unused-var warnings created by the change)
- `npm run test:run` → 21/21 pass. Specifically,
  `test/components/Layout.test.js`'s 5 regression-lock assertions for
  the logged-out Layout branch (Gotcha #8) continue to pass — they
  don't assert on `MobileNavigation`'s prop shape, so the dead-prop
  removal is invisible to the suite
- CI on PR #28: Lint ✓ | Vitest 21/21 ✓ | Playwright smoke 3/3 ✓ |
  `forbidden-endpoints` ✓ | `forbidden-cors-headers` ✓ | Vercel
  preview deploy ✓ | Aggregate gate ✓
- `Screenshot diff`: triggered (PR #28 touches `components/**` which
  matches the visual-diff path filter) — `continue-on-error: true`
  swallow per the documented Decision-4 end state of
  `adopt-playwright-smoke` (no baseline committed yet). Not a
  regression; the dead-prop removal cannot move a single rendered
  pixel because the prop was never read.

**Out-of-scope items preserved as documented:**
- Pre-existing dead `import { useState } from 'react'` on
  `components/MobileNavigation.js` line 3 — untouched per the convoy
  spec's "single-prop removal" boundary. A future hygiene pass can
  sweep it.
- `components/Layout.js.backup` — untouched per no-go-zone rule.

**Operator action required going forward:** **none.** No env vars, no
schema, no infra.

**Spec deviation:** none.

**No follow-up surfaced.** The convoy's documented R1 (hypothetical
future feature that wants per-user bottom-bar items) is an "accept
the cleanup, pay the add-the-prop cost when the feature actually
lands" call — not a queued follow-up.
