wulf-pulse/.planning/phases/02-mobile-shell-more-drawer/02-02-PLAN.md
lorentz 9658640c04 fix(04-01): restore phase 2/3 work lost by worktree soft-reset
The soft reset to 77073ba inadvertently staged deletions of all phase 2
and 3 artifacts. This commit restores them from their source commits so
subsequent task commits build on the complete prior-phase foundation:
- components/mobile/{BottomNav,HeaderBar,KpiCardMobile,MoreDrawer,NeedsAttentionStrip,WorkerStatusRow}
- app/mobile/layout.tsx, dashboard/page.tsx, analyzer/page.tsx
- app/api/mobile/dashboard/route.ts
- All .planning/** files from phases 01-04
- CLAUDE.md, app/layout.tsx, app/styles/brand.css, public/manifest.json
2026-05-03 18:01:14 -04:00

22 KiB
Raw Blame History

phase plan type wave depends_on files_modified autonomous requirements must_haves
02-mobile-shell-more-drawer 02 execute 2
02-01
app/mobile/layout.tsx
app/mobile/nav/page.tsx
false
SHELL-01
SHELL-05
DRAWER-06
truths artifacts key_links
app/mobile/layout.tsx is rewritten to import HeaderBar, BottomNav, MoreDrawer from components/mobile/* and renders them around <main>{children}</main>
The layout owns a single React.useState boolean that opens/closes the MoreDrawer; HeaderBar's onAvatarClick and BottomNav's onMoreClick both flip this state to true
The <main> content area scrolls and has bottom padding equal to bottom-nav height (h-16 = 64px) plus env(safe-area-inset-bottom) so content does not hide under the nav
app/mobile/nav/page.tsx no longer exists — the file is deleted in this same change
Visiting /mobile/dashboard, /mobile/tickets, /mobile/finance, and /mobile/analyzer all render inside the new layout (header + bottom nav visible, no 404)
TypeScript compiles (npx tsc --noEmit) and Next.js builds (npm run build) successfully
path provides contains
app/mobile/layout.tsx New mobile shell wiring HeaderBar + BottomNav + MoreDrawer with shared drawer state MoreDrawer
path provides deleted
app/mobile/nav/page.tsx DELETED — drawer fully replaces the standalone nav page (DRAWER-06) true
from to via pattern
app/mobile/layout.tsx components/mobile/HeaderBar.tsx import + render with onAvatarClick from ['"]@/components/mobile/HeaderBar['"]
from to via pattern
app/mobile/layout.tsx components/mobile/BottomNav.tsx import + render with onMoreClick from ['"]@/components/mobile/BottomNav['"]
from to via pattern
app/mobile/layout.tsx components/mobile/MoreDrawer.tsx import + render with shared open/onOpenChange state from ['"]@/components/mobile/MoreDrawer['"]
from to via pattern
Header avatar AND Bottom-nav More button MoreDrawer open state Single useState in app/mobile/layout.tsx useState
Replace `app/mobile/layout.tsx` with the new shell that wires HeaderBar + BottomNav + MoreDrawer (built in Plan 01) around `{children}`, owning a single shared drawer-open state. Delete `app/mobile/nav/page.tsx` in the same change so the drawer fully replaces the old standalone nav page.

Purpose: Land SHELL-01 (replace in place), SHELL-05 (scrollable content with bottom-nav-aware padding), and DRAWER-06 (delete the old nav route). After this plan, every /mobile/* page renders under the new shell and the four primary tabs + avatar + More all behave per spec.

Output: Modified app/mobile/layout.tsx, deleted app/mobile/nav/page.tsx. Build passes. Visual checkpoint confirms the shell renders correctly on at least one mobile route.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/ROADMAP.md @.planning/REQUIREMENTS.md @.planning/phases/02-mobile-shell-more-drawer/02-CONTEXT.md @.planning/phases/02-mobile-shell-more-drawer/02-01-PLAN.md @docs/superpowers/specs/2026-05-03-mobile-shell-design.md @CLAUDE.md @app/mobile/layout.tsx @app/mobile/nav/page.tsx @app/styles/brand.css

From components/mobile/HeaderBar.tsx (Plan 01):

export function HeaderBar(props: { onAvatarClick: () => void }): JSX.Element;

From components/mobile/BottomNav.tsx (Plan 01):

export function BottomNav(props: { onMoreClick: () => void }): JSX.Element;

From components/mobile/MoreDrawer.tsx (Plan 01):

export function MoreDrawer(props: {
  open: boolean;
  onOpenChange: (open: boolean) => void;
}): JSX.Element;

CSS utilities available in app/styles/brand.css:

  • pt-safe, pb-safe

<scope_boundary> This plan only touches:

  • app/mobile/layout.tsx (full rewrite)
  • app/mobile/nav/page.tsx (delete)

Do NOT modify:

  • The three new components (Plan 01 owns them)
  • Any page under app/mobile/dashboard|tickets|finance|analyzer|page.tsx (out of phase)
  • components/navigation/app-navigation.tsx (desktop nav)
  • app/layout.tsx (root, owned by Phase 1) </scope_boundary>
Task 1: Rewrite app/mobile/layout.tsx to wire HeaderBar + BottomNav + MoreDrawer with shared state app/mobile/layout.tsx - app/mobile/layout.tsx (current 3-tab layout being replaced — read fully so executor knows what's there) - components/mobile/HeaderBar.tsx (Plan 01 output — confirms onAvatarClick prop) - components/mobile/BottomNav.tsx (Plan 01 output — confirms onMoreClick prop) - components/mobile/MoreDrawer.tsx (Plan 01 output — confirms open/onOpenChange props) - app/styles/brand.css (confirm `pt-safe` and `pb-safe` are available) - .planning/phases/02-mobile-shell-more-drawer/02-CONTEXT.md (SHELL-05: bottom padding = nav height + safe-area) **Replace the entire contents** of `app/mobile/layout.tsx` with:
'use client';

/* Mobile shell — phase 02 (SHELL-01, SHELL-05).
 *
 * Header: <HeaderBar /> (sticky, brand + Bell + avatar)
 * Body:   <main> (scrollable, padded so content clears the bottom nav)
 * Foot:   <BottomNav /> (fixed, 4 tabs + More)
 * Drawer: <MoreDrawer /> opened from BOTH the header avatar and the More cell.
 *
 * The drawer's open state lives here so a single Sheet instance is shared
 * between the two triggers — no duplicate Sheets, no prop-drilling sagas. */

import { useState } from 'react';
import { HeaderBar } from '@/components/mobile/HeaderBar';
import { BottomNav } from '@/components/mobile/BottomNav';
import { MoreDrawer } from '@/components/mobile/MoreDrawer';

export default function MobileLayout({ children }: { children: React.ReactNode }) {
  const [drawerOpen, setDrawerOpen] = useState(false);

  return (
    <div className="flex flex-col min-h-screen bg-background max-w-lg mx-auto">
      <HeaderBar onAvatarClick={() => setDrawerOpen(true)} />

      {/* SHELL-05: scrollable content area; bottom padding = bottom-nav (h-16
          = 64px = pb-16) plus the device safe-area inset, so content never
          hides under the bar. */}
      <main className="flex-1 overflow-y-auto pb-[calc(theme(spacing.16)+env(safe-area-inset-bottom))]">
        {children}
      </main>

      <BottomNav onMoreClick={() => setDrawerOpen(true)} />

      <MoreDrawer open={drawerOpen} onOpenChange={setDrawerOpen} />
    </div>
  );
}

Notes:

  • This file replaces the existing 3-tab layout entirely. No legacy imports, no dead code paths.
  • 'use client' is required because we use useState.
  • The pb-[calc(...)] arbitrary value gives <main> enough bottom padding to clear the 64px nav plus the home-indicator inset (SHELL-05 + PWA-04 reuse). Tailwind 4 supports the calc() arbitrary value here.
  • A single useState is the entire shared-state mechanism — no Zustand, no Context, no third-party state lib (per CLAUDE.md "no new state libraries").
  • Both triggers set the same boolean. The MoreDrawer itself controls its close (Radix onOpenChange fires when overlay is clicked or Esc is pressed) and propagates back through setDrawerOpen. grep -q "from '@/components/mobile/HeaderBar'" app/mobile/layout.tsx && grep -q "from '@/components/mobile/BottomNav'" app/mobile/layout.tsx && grep -q "from '@/components/mobile/MoreDrawer'" app/mobile/layout.tsx && grep -q "useState" app/mobile/layout.tsx && grep -q "onAvatarClick" app/mobile/layout.tsx && grep -q "onMoreClick" app/mobile/layout.tsx && grep -q "drawerOpen" app/mobile/layout.tsx && grep -q "safe-area-inset-bottom" app/mobile/layout.tsx <acceptance_criteria>
    • grep -E "from ['\"]@/components/mobile/HeaderBar['\"]" app/mobile/layout.tsx matches
    • grep -E "from ['\"]@/components/mobile/BottomNav['\"]" app/mobile/layout.tsx matches
    • grep -E "from ['\"]@/components/mobile/MoreDrawer['\"]" app/mobile/layout.tsx matches
    • grep -E "useState" app/mobile/layout.tsx matches (single shared state)
    • grep -E "onAvatarClick" app/mobile/layout.tsx matches AND grep -E "onMoreClick" app/mobile/layout.tsx matches (both triggers wired)
    • grep -E "open=" app/mobile/layout.tsx matches AND grep -E "onOpenChange=" app/mobile/layout.tsx matches (drawer controlled)
    • grep -E "safe-area-inset-bottom" app/mobile/layout.tsx matches (SHELL-05 padding for bottom nav clearance)
    • grep -E "max-w-lg mx-auto" app/mobile/layout.tsx matches (CONTEXT.md container width)
    • ! grep -E "Menu, " app/mobile/layout.tsx exits 0 (the legacy Menu-as-link import from the old layout is gone)
    • ! grep -E "/mobile/nav" app/mobile/layout.tsx exits 0 (no link to the deleted standalone nav route) </acceptance_criteria> The layout renders the three new components, owns a single useState for drawer open/close, and pads <main> to clear the bottom nav + safe area.
Task 2: Delete app/mobile/nav/page.tsx (DRAWER-06) app/mobile/nav/page.tsx - app/mobile/nav/page.tsx (final read of the file being deleted, so the executor knows what is leaving the codebase) - .planning/phases/02-mobile-shell-more-drawer/02-CONTEXT.md (DRAWER-06 — delete in same change as drawer ships; no redirect) - components/mobile/MoreDrawer.tsx (Plan 01 output — verifies the drawer already covers everything the old page did) Delete the file:
rm app/mobile/nav/page.tsx

Then check the directory is empty (or only contains other files we don't care about) and remove it if it became empty:

# If app/mobile/nav is now empty, remove the directory too.
if [ -d app/mobile/nav ] && [ -z "$(ls -A app/mobile/nav)" ]; then
  rmdir app/mobile/nav
fi

Notes:

  • Per CONTEXT.md DRAWER-06: "Recommend NO redirect (just delete) — the URL was never bookmarked-worthy." Visiting /mobile/nav after this change yields Next.js's standard 404, which is the desired behavior.
  • Confirm no other file in the repo references /mobile/nav or imports from app/mobile/nav/.... Run a quick grep before deletion (the old app/mobile/layout.tsx had the only known reference, and Task 1 already removed it). ! test -f app/mobile/nav/page.tsx <acceptance_criteria>
    • ! test -f app/mobile/nav/page.tsx exits 0 (file deleted)
    • ! grep -r --include="*.ts" --include="*.tsx" "/mobile/nav" app components 2>/dev/null exits 0 (no remaining references in source)
    • The deletion shows up in git status as a deleted file </acceptance_criteria> app/mobile/nav/page.tsx no longer exists; no source file references /mobile/nav anywhere.
Task 3: Type-check and full build to confirm the new shell compiles end-to-end (no files written — gate task) - app/mobile/layout.tsx (the file just rewritten) - components/mobile/HeaderBar.tsx (Plan 01) - components/mobile/BottomNav.tsx (Plan 01) - components/mobile/MoreDrawer.tsx (Plan 01) Run:
npx tsc --noEmit --pretty
npm run build

Both must exit 0. If either fails, fix the offending file and rerun until clean. Common things to check if it fails:

  • Did Task 2 leave a dangling import to the deleted nav/page.tsx? (Should be impossible, but grep /mobile/nav if a build error names that path.)
  • Did the 'use client' directive end up below an import? (Must be the very first line.)
  • Is the pb-[calc(theme(spacing.16)+env(safe-area-inset-bottom))] Tailwind 4 arbitrary value valid? If Tailwind rejects it, fall back to an inline style on the <main>: style={{ paddingBottom: 'calc(4rem + env(safe-area-inset-bottom))' }} and remove the pb-[...] class.

Do NOT modify any file other than app/mobile/layout.tsx to fix build issues. npx tsc --noEmit --pretty && npm run build <acceptance_criteria> - npx tsc --noEmit --pretty exits 0 - npm run build exits 0 - git status --short app components shows: 1 modified (app/mobile/layout.tsx) and 1 deleted (app/mobile/nav/page.tsx); no other unexpected modifications </acceptance_criteria> Type-check + build both pass with the new shell wired and the old nav page deleted.

Task 4: Visual verification of the new mobile shell on a real device or DevTools mobile preview (no files written — human checkpoint) - app/mobile/layout.tsx (the file just rewritten — executor confirms what was shipped) - .planning/ROADMAP.md (Phase 2 success criteria #16 — these are what the human is verifying) - .planning/REQUIREMENTS.md (SHELL-01..06, NAV-01..03, DRAWER-01..06) Pause and surface a checkpoint to the user. Present this exact verification script and wait for the user's "approved" response.

What was built (summary for the user): The new mobile shell is fully wired:

  • app/mobile/layout.tsx rewritten — sticky header (Wulf mark + "Pulse" wordmark, Bell, avatar), scrollable <main>, fixed bottom nav (Dashboard / Tickets / Finance / Analyzer / More)
  • app/mobile/nav/page.tsx deleted
  • app/mobile/analyzer/page.tsx placeholder ("Coming soon" card) so the Analyzer tab resolves until Phase 6
  • <MoreDrawer /> opens from BOTH the header avatar and the bottom-nav More button, with three sections (Mobile sections / Full site / Account + Sign out)

All built on existing shadcn primitives, the WulfMark component, and Better Auth's signOut() — no new state libs, no shadcn avatar primitive added.

How to verify (user runs through this on a phone-sized viewport):

  1. Start the dev server if not already running: npm run dev. Pulse should start on http://localhost:3100.

  2. Open the mobile shell in a phone-sized viewport — Chrome DevTools (F12) → toggle device toolbar (Ctrl+Shift+M / Cmd+Shift+M) → pick "iPhone 15 Pro" or any 390-414px wide device. Visit http://localhost:3100/mobile/dashboard.

  3. Verify the header (SHELL-02..04):

    • Sticky bar at the top with bg-background/95 backdrop-blur + bottom border
    • Left side: Wulf "W" mark + "Pulse" wordmark; tapping it navigates to /mobile/dashboard
    • Right side: a Bell icon button next to a small avatar circle (initials)
    • No page title text in the header itself
    • Bell button is keyboard-focusable (Tab to it, then Space/Enter — should not throw or navigate; it's a placeholder, no menu)
    • Tapping the avatar opens the right-side Sheet drawer
  4. Verify the bottom nav (SHELL-06, NAV-01..03):

    • Fixed bar at the bottom, full width, border-t bg-background
    • Five cells in order: Dashboard, Tickets, Finance, Analyzer, More
    • Active tab uses text-primary (Wulf blue); inactive use text-muted-foreground
    • Tapping each tab routes to its URL: /mobile/dashboard, /mobile/tickets, /mobile/finance, /mobile/analyzer
    • Tapping a row INSIDE /mobile/tickets/[id] (e.g., open any ticket) keeps Tickets highlighted (active detection via pathname.startsWith)
    • Tapping More opens the same drawer the avatar opens
    • Visiting /mobile/analyzer shows the "Coming soon" placeholder card (NOT a 404)
  5. Verify the drawer (DRAWER-01..05): Open the drawer (avatar OR More).

    • Drawer slides in from the right (side="right")
    • Section 1 "Mobile sections" — single row: Engagement (no ExternalLink hint icon)
    • Section 2 "Full site" — five rows: Quotes, Configuration Items, Backup Status, Ticket Digest, Admin / Sync (each row has the ExternalLink icon on the right)
    • Section 3 "Account" — shows the signed-in user's initials, name, and email; below it, a red "Sign out" button
    • Tapping any row inside the drawer navigates AND closes the drawer
    • Tapping the X / outside the drawer / pressing Esc closes it
    • Sign out: tap it → page navigates to /auth/sign-in AND the user is signed out (refreshing brings you to the sign-in page; no auto-redirect to /mobile)
  6. Verify content does not hide under the bottom nav (SHELL-05):

    • On /mobile/dashboard (or any mobile page), scroll to the bottom of the content. The last visible content sits ABOVE the bottom nav, not under it.
    • On a phone with a home indicator (or in DevTools with iPhone preset), the bottom nav has extra space below for the indicator inset (no overlap).
  7. Verify the old nav route is gone (DRAWER-06):

    • Visit http://localhost:3100/mobile/nav directly. It returns Next.js's 404 page (NOT the old standalone nav UI).
  8. Quick regression on existing pages:

    • /mobile/dashboard, /mobile/tickets, /mobile/finance all still render their previous content unchanged — only the chrome around them is new.
    • /mobile/tickets/[id] (open a ticket) still renders inside the new shell.

Resume signal: Reply "approved" once all checks pass. If something is broken or off-spec, describe what you saw and which check failed (e.g., "Drawer opens from the bottom, not the right" or "Bottom nav overlaps the last content row on /mobile/finance"). The executor will fix and re-verify. echo "Manual verification — user must reply 'approved' or describe a failure. No automated check applicable; preceding tasks (1-3) verify code-level invariants." <acceptance_criteria> - User replies "approved" after running the verification script above - All 8 verification sections pass on the user's device/preview - If any check fails, the executor returns to Task 1 or Task 2 to fix and re-runs Task 3 (build) and Task 4 (re-verify) before requesting approval again </acceptance_criteria> User has explicitly replied "approved", confirming the new shell renders correctly on a phone-sized viewport and all 6 ROADMAP success criteria for Phase 2 are met.

<threat_model>

Trust Boundaries

Boundary Description
Browser → drawer Sign out Reuses Plan 01's MoreDrawer; calls Better Auth signOut() and navigates to /auth/sign-in — same trust boundary as the existing top-bar UserMenu.
Browser → all Link routes All routes already exist or are placeholders (/mobile/analyzer placeholder shipped in Plan 01). No new endpoints.

STRIDE Threat Register (ASVS-L1 baseline)

Threat ID Category Component Disposition Mitigation Plan
T-02-05 Tampering layout.tsx drawer-state useState accept Local React state, not URL-driven. An attacker cannot pre-open the drawer via crafted URL. State has no security relevance — it merely toggles UI visibility.
T-02-06 Information Disclosure Deletion of /mobile/nav route accept The deleted page surfaced no PII beyond what the new drawer surfaces (same email field). Net change: identical surface area.
T-02-07 Denial of Service New shell mounts on every /mobile/* request accept Layout is lightweight: 1 useState, 3 component imports, no fetches. Cost is negligible vs. the existing layout.
T-02-08 Repudiation Sign out action mitigate Better Auth records sign-out in its session table; not a Pulse-introduced repudiation surface. Inherited from lib/auth-client.ts.
</threat_model>
After this plan completes:
  1. app/mobile/layout.tsx imports HeaderBar, BottomNav, MoreDrawer:
    • grep -E "@/components/mobile/HeaderBar" app/mobile/layout.tsx matches
    • grep -E "@/components/mobile/BottomNav" app/mobile/layout.tsx matches
    • grep -E "@/components/mobile/MoreDrawer" app/mobile/layout.tsx matches
  2. app/mobile/nav/page.tsx does not exist:
    • ! test -f app/mobile/nav/page.tsx exits 0
  3. No source file references /mobile/nav:
    • ! grep -r --include="*.ts" --include="*.tsx" "/mobile/nav" app components 2>/dev/null exits 0
  4. Build is clean:
    • npx tsc --noEmit --pretty exits 0
    • npm run build exits 0
  5. Visual checkpoint passed (Task 4):
    • Header sticky, brand link goes to /mobile/dashboard, Bell focusable with no menu, avatar opens drawer
    • Bottom nav shows 5 cells with correct icons + routes; active tab uses text-primary; /mobile/tickets/123 highlights Tickets
    • Drawer (side="right") shows 3 sections; Sign out signs out and lands on /auth/sign-in
    • /mobile/analyzer renders the placeholder, NOT a 404
    • /mobile/nav returns 404
    • Content does not hide under the bottom nav (SHELL-05)

<success_criteria>

  • All 4 tasks complete (3 auto + 1 visual checkpoint with explicit "approved")
  • app/mobile/layout.tsx is rewritten to use the new components with shared drawer state
  • app/mobile/nav/page.tsx is deleted
  • Type-check + build both pass
  • Visual checkpoint approved by user
  • Phase 2 ROADMAP success criteria #16 are all satisfied (header, 5-cell nav, drawer with 3 sections, sign-out flow, /mobile/nav gone, content not hidden under bar)
  • All 15 phase requirements (SHELL-01..06, NAV-01..03, DRAWER-01..06) are now closed across Plan 01 + Plan 02 </success_criteria>
After completion, create `.planning/phases/02-mobile-shell-more-drawer/02-02-SUMMARY.md` documenting: - Which requirements this plan addressed (SHELL-01, SHELL-05, DRAWER-06) and confirmation that combined with Plan 01 all 15 phase requirements are now satisfied - The final wiring (layout owns one `useState`, both triggers share it) - Any deviations from the plan during execution (e.g., if `pb-[calc(...)]` had to fall back to inline style) - Visual checkpoint outcomes (which checks passed, any minor adjustments made) - Notes for Phases 37: every mobile page lands inside this shell automatically; pages should NOT add their own header or bottom nav