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
22 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-mobile-shell-more-drawer | 02 | execute | 2 |
|
|
false |
|
|
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.cssFrom 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>
'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 useuseState.- 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 thecalc()arbitrary value here. - A single
useStateis 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
MoreDraweritself controls its close (RadixonOpenChangefires when overlay is clicked or Esc is pressed) and propagates back throughsetDrawerOpen. 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.tsxmatchesgrep -E "from ['\"]@/components/mobile/BottomNav['\"]" app/mobile/layout.tsxmatchesgrep -E "from ['\"]@/components/mobile/MoreDrawer['\"]" app/mobile/layout.tsxmatchesgrep -E "useState" app/mobile/layout.tsxmatches (single shared state)grep -E "onAvatarClick" app/mobile/layout.tsxmatches ANDgrep -E "onMoreClick" app/mobile/layout.tsxmatches (both triggers wired)grep -E "open=" app/mobile/layout.tsxmatches ANDgrep -E "onOpenChange=" app/mobile/layout.tsxmatches (drawer controlled)grep -E "safe-area-inset-bottom" app/mobile/layout.tsxmatches (SHELL-05 padding for bottom nav clearance)grep -E "max-w-lg mx-auto" app/mobile/layout.tsxmatches (CONTEXT.md container width)! grep -E "Menu, " app/mobile/layout.tsxexits 0 (the legacyMenu-as-link import from the old layout is gone)! grep -E "/mobile/nav" app/mobile/layout.tsxexits 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.
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/navafter this change yields Next.js's standard 404, which is the desired behavior. - Confirm no other file in the repo references
/mobile/navor imports fromapp/mobile/nav/.... Run a quick grep before deletion (the oldapp/mobile/layout.tsxhad 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.tsxexits 0 (file deleted)! grep -r --include="*.ts" --include="*.tsx" "/mobile/nav" app components 2>/dev/nullexits 0 (no remaining references in source)- The deletion shows up in
git statusas a deleted file </acceptance_criteria>app/mobile/nav/page.tsxno longer exists; no source file references/mobile/navanywhere.
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/navif 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 thepb-[...]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.
What was built (summary for the user): The new mobile shell is fully wired:
app/mobile/layout.tsxrewritten — sticky header (Wulf mark + "Pulse" wordmark, Bell, avatar), scrollable<main>, fixed bottom nav (Dashboard / Tickets / Finance / Analyzer / More)app/mobile/nav/page.tsxdeletedapp/mobile/analyzer/page.tsxplaceholder ("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):
-
Start the dev server if not already running:
npm run dev. Pulse should start on http://localhost:3100. -
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. -
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
- Sticky bar at the top with
-
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 usetext-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 viapathname.startsWith) - Tapping More opens the same drawer the avatar opens
- Visiting
/mobile/analyzershows the "Coming soon" placeholder card (NOT a 404)
- Fixed bar at the bottom, full width,
-
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
ExternalLinkhint icon) - Section 2 "Full site" — five rows: Quotes, Configuration Items, Backup Status, Ticket Digest, Admin / Sync (each row has the
ExternalLinkicon 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-inAND the user is signed out (refreshing brings you to the sign-in page; no auto-redirect to/mobile)
- Drawer slides in from the right (
-
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).
- On
-
Verify the old nav route is gone (DRAWER-06):
- Visit
http://localhost:3100/mobile/navdirectly. It returns Next.js's 404 page (NOT the old standalone nav UI).
- Visit
-
Quick regression on existing pages:
/mobile/dashboard,/mobile/tickets,/mobile/financeall 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> |
app/mobile/layout.tsximports HeaderBar, BottomNav, MoreDrawer:grep -E "@/components/mobile/HeaderBar" app/mobile/layout.tsxmatchesgrep -E "@/components/mobile/BottomNav" app/mobile/layout.tsxmatchesgrep -E "@/components/mobile/MoreDrawer" app/mobile/layout.tsxmatches
app/mobile/nav/page.tsxdoes not exist:! test -f app/mobile/nav/page.tsxexits 0
- No source file references
/mobile/nav:! grep -r --include="*.ts" --include="*.tsx" "/mobile/nav" app components 2>/dev/nullexits 0
- Build is clean:
npx tsc --noEmit --prettyexits 0npm run buildexits 0
- 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/123highlights Tickets - Drawer (
side="right") shows 3 sections; Sign out signs out and lands on/auth/sign-in /mobile/analyzerrenders the placeholder, NOT a 404/mobile/navreturns 404- Content does not hide under the bottom nav (SHELL-05)
- Header sticky, brand link goes to
<success_criteria>
- All 4 tasks complete (3 auto + 1 visual checkpoint with explicit "approved")
app/mobile/layout.tsxis rewritten to use the new components with shared drawer stateapp/mobile/nav/page.tsxis deleted- Type-check + build both pass
- Visual checkpoint approved by user
- Phase 2 ROADMAP success criteria #1–6 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>