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
20 KiB
| phase | verified | status | score | human_verification | |||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-mobile-shell-more-drawer | 2026-05-03T20:30:00Z | human_needed | 15/15 must-haves verified |
|
Phase 2: Mobile Shell + More Drawer Verification Report
Phase Goal: Every /mobile/* page renders inside a new layout — sticky header (Wulf mark + Bell placeholder + avatar), scrollable content, and a 5-cell bottom nav whose fifth control opens a Sheet drawer that fully replaces /mobile/nav.
Verified: 2026-05-03T20:30:00Z
Status: human_needed
Re-verification: No — initial verification
Goal Achievement
Observable Truths
| # | Truth | Status | Evidence |
|---|---|---|---|
| 1 | app/mobile/layout.tsx mounts HeaderBar + BottomNav + MoreDrawer with shared useState drawer state |
VERIFIED | layout.tsx imports all three; single drawerOpen state; both onAvatarClick and onMoreClick call () => setDrawerOpen(true); MoreDrawer open={drawerOpen} onOpenChange={setDrawerOpen} |
| 2 | HeaderBar has Wulf mark + Pulse wordmark linked to /mobile/dashboard, Bell placeholder (aria-label="Notifications", empty onClick), avatar trigger | VERIFIED | WulfMark variant="mark", <span>Pulse</span>, href="/mobile/dashboard", aria-label="Notifications" with empty onClick body, avatar button calling onAvatarClick |
| 3 | <main> has bottom padding clearing bottom nav height (h-16=64px) + safe-area inset |
VERIFIED | pb-[calc(theme(spacing.16)+env(safe-area-inset-bottom))] on <main> in layout.tsx line 28 |
| 4 | BottomNav has 4 link tabs (Dashboard, Tickets, Finance, Analyzer) + More button = 5 cells | VERIFIED | TABS const has 4 entries with LayoutDashboard/Ticket/DollarSign/Sparkles icons; fifth <button> with Menu icon labeled "More" |
| 5 | /mobile/analyzer route exists and does not 404 | VERIFIED | app/mobile/analyzer/page.tsx exists, exports MobileAnalyzerPlaceholder, renders "Analyzer feed coming soon" card; no data fetching |
| 6 | Active tab uses pathname.startsWith(href) |
VERIFIED | Line 46: const active = pathname?.startsWith(href) ?? false — handles nested routes like /mobile/tickets/123 |
| 7 | MoreDrawer is a single Sheet opened by both header avatar AND More button (shared state) | VERIFIED | Single useState(false) in layout.tsx; both trigger callbacks set it true; MoreDrawer receives controlled open/onOpenChange props |
| 8 | Drawer uses side="right" |
VERIFIED | <SheetContent side="right" ...> at MoreDrawer.tsx line 79 |
| 9 | Mobile sections row = Engagement only, NO ExternalLink icon | VERIFIED | MOBILE_SECTIONS array has only /mobile/engagement; its JSX template renders <Icon> + <span> with no ExternalLink; ExternalLink only renders in DESKTOP_LINKS map |
| 10 | Full site rows have ExternalLink hint icons | VERIFIED | DESKTOP_LINKS map renders <ExternalLink className="w-3.5 h-3.5 ..."> after each link label (line 121) |
| 11 | Account section: user identity (read-only) + Sign out via Better Auth | VERIFIED | useSession() populates user; initials + name + email rendered; handleSignOut() calls await signOut() then router.push('/auth/sign-in') |
| 12 | app/mobile/nav/page.tsx deleted |
VERIFIED | test -f app/mobile/nav/page.tsx exits non-zero; no references to /mobile/nav found in any .ts/.tsx file |
| 13 | TypeScript compiles clean | VERIFIED | npx tsc --noEmit --pretty exits 0 with no output |
| 14 | WulfMark component wired from correct import | VERIFIED | from '@/components/branding/wulf-mark'; components/branding/wulf-mark.tsx exports WulfMark; variant="mark" used |
| 15 | Sheet + auth-client properly imported in MoreDrawer | VERIFIED | from '@/components/ui/sheet' with SheetContent/SheetHeader/SheetTitle/SheetDescription/SheetClose; from '@/lib/auth-client' with useSession, signOut |
Score: 15/15 truths verified
Required Artifacts
| Artifact | Expected | Status | Details |
|---|---|---|---|
app/mobile/layout.tsx |
New shell — HeaderBar + BottomNav + MoreDrawer with shared state | VERIFIED | 38 lines; imports all three components; useState; both triggers wired; safe-area padding on main |
components/mobile/HeaderBar.tsx |
Sticky header — WulfMark+wordmark link, Bell placeholder, avatar trigger | VERIFIED | 72 lines; sticky top-0; bg-background/95 backdrop-blur; border-b; pt-safe; WulfMark; Pulse wordmark; Bell; h-7 w-7 avatar; onAvatarClick |
components/mobile/BottomNav.tsx |
Fixed bottom tab bar — 4 tabs + More button | VERIFIED | 76 lines; fixed bottom-0; border-t; max-w-lg mx-auto; h-16; 4 TABS const + More button; pathname.startsWith; pb-safe |
components/mobile/MoreDrawer.tsx |
shadcn Sheet (side=right) with 3 sections | VERIFIED | 167 lines; Sheet side=right; controlled open/onOpenChange; 3 named sections; signOut + /auth/sign-in |
app/mobile/analyzer/page.tsx |
Placeholder route so Analyzer tab resolves | VERIFIED | Server component; "Analyzer feed coming soon" card; no fetch calls |
app/mobile/nav/page.tsx |
DELETED | VERIFIED | File does not exist; no references in source files |
Key Link Verification
| From | To | Via | Status | Details |
|---|---|---|---|---|
app/mobile/layout.tsx |
components/mobile/HeaderBar.tsx |
import + render with onAvatarClick | WIRED | Line 14+23: imported and rendered with onAvatarClick={() => setDrawerOpen(true)} |
app/mobile/layout.tsx |
components/mobile/BottomNav.tsx |
import + render with onMoreClick | WIRED | Line 15+32: imported and rendered with onMoreClick={() => setDrawerOpen(true)} |
app/mobile/layout.tsx |
components/mobile/MoreDrawer.tsx |
import + render with shared open/onOpenChange | WIRED | Line 16+34: <MoreDrawer open={drawerOpen} onOpenChange={setDrawerOpen} /> |
components/mobile/HeaderBar.tsx |
components/branding/wulf-mark.tsx |
WulfMark import (variant='mark') | WIRED | Line 14+43: imported and rendered <WulfMark variant="mark" className="h-6 w-auto" /> |
components/mobile/MoreDrawer.tsx |
components/ui/sheet.tsx |
Sheet, SheetContent, SheetTrigger imports | WIRED | Line 26-32: all Sheet primitives imported; <Sheet open={open}> + <SheetContent side="right"> used |
components/mobile/MoreDrawer.tsx |
lib/auth-client.ts |
signOut + useSession imports | WIRED | Line 33: import { useSession, signOut } from '@/lib/auth-client'; both called in component body |
components/mobile/BottomNav.tsx |
/mobile/analyzer |
Analyzer tab href | WIRED | Line 29: { href: '/mobile/analyzer', label: 'Analyzer', icon: Sparkles } in TABS |
| Header avatar AND Bottom-nav More button | MoreDrawer open state | Single useState in layout.tsx | WIRED | Both callbacks reference same drawerOpen/setDrawerOpen; single Sheet instance |
Data-Flow Trace (Level 4)
Not applicable. These are pure UI shell components. No dynamic data is fetched in this phase — the layout, header, and bottom nav render purely from client-side React state and the existing Better Auth session (which is an ambient resource, not a fetch introduced by this phase).
Behavioral Spot-Checks
| Behavior | Command | Result | Status |
|---|---|---|---|
| TypeScript compiles with all 5 new files | npx tsc --noEmit --pretty |
Exit 0, no output | PASS |
app/mobile/nav/page.tsx deleted, no references |
test -f app/mobile/nav/page.tsx |
Exit 1 (non-existent) | PASS |
No /mobile/nav references in source |
grep -rn "/mobile/nav" app/ components/ |
Zero matches | PASS |
| MoreDrawer side=right locked | grep -n 'side="right"' components/mobile/MoreDrawer.tsx |
Line 79: <SheetContent side="right" |
PASS |
| Engagement in MOBILE_SECTIONS (no ExternalLink) | Read MoreDrawer.tsx section 1 JSX | No ExternalLink in MOBILE_SECTIONS map template | PASS |
| Shared state (not two Sheet instances) | grep -n "Sheet|useState" app/mobile/layout.tsx |
Single useState, single MoreDrawer render | PASS |
Requirements Coverage
| Requirement | Source Plan | Description | Status | Evidence |
|---|---|---|---|---|
| SHELL-01 | 02-02 | New layout.tsx replaces current layout (rebuild in place) | SATISFIED | layout.tsx rewritten in-place; imports HeaderBar/BottomNav/MoreDrawer |
| SHELL-02 | 02-01 | Sticky header: bg-background/95 backdrop-blur + border-b; WulfMark + Pulse wordmark → /mobile/dashboard | SATISFIED | HeaderBar line 35: sticky top-0 z-30 bg-background/95 backdrop-blur ... border-b pt-safe; WulfMark + wordmark linked to /mobile/dashboard |
| SHELL-03 | 02-01 | Bell icon button (placeholder, no menu/badge, aria-label="Notifications", empty onClick) | SATISFIED | HeaderBar lines 50-55: aria-label="Notifications", empty onClick comment /* SHELL-03: placeholder */ |
| SHELL-04 | 02-01 | Compact avatar h-7 w-7; tapping opens More drawer | SATISFIED | HeaderBar line 64: h-7 w-7 items-center justify-center rounded-full; onClick={onAvatarClick} |
| SHELL-05 | 02-02 | <main> scrollable with bottom padding = bottom-nav height + safe-area |
SATISFIED | layout.tsx line 28: pb-[calc(theme(spacing.16)+env(safe-area-inset-bottom))] on <main> |
| SHELL-06 | 02-01 | Fixed bottom nav: border-t bg-background, max-w-lg mx-auto, 5 cells | SATISFIED | BottomNav line 42-44: fixed bottom-0 ... border-t bg-background pb-safe; max-w-lg mx-auto flex h-16; 4 tabs + More |
| NAV-01 | 02-01 | 4 equal-width primary tabs: Dashboard/Tickets/Finance/Analyzer with correct icons | SATISFIED | TABS const with LayoutDashboard/Ticket/DollarSign/Sparkles; all flex-1 |
| NAV-02 | 02-01 | Tabs route to /mobile/dashboard, /mobile/tickets, /mobile/finance, /mobile/analyzer | SATISFIED | TABS hrefs verified; /mobile/analyzer page exists (no 404) |
| NAV-03 | 02-01 | Active=text-primary, inactive=text-muted-foreground; detection via pathname.startsWith(href) | SATISFIED | BottomNav lines 46+53-55: startsWith check; text-primary when active |
| DRAWER-01 | 02-01 | Fifth bottom-bar control "More" with Menu icon opens Sheet | SATISFIED | BottomNav More button with Menu icon; calls onMoreClick → setDrawerOpen(true) → Sheet opens |
| DRAWER-02 | 02-01 | Sheet uses consistent side: right | SATISFIED | MoreDrawer line 79: side="right" (locked decision per CONTEXT.md) |
| DRAWER-03 | 02-01 | "Mobile sections": Engagement (/mobile/engagement), no ExternalLink icon | SATISFIED | MOBILE_SECTIONS = [{href: '/mobile/engagement', ...}]; section 1 JSX has no ExternalLink in template |
| DRAWER-04 | 02-01 | "Full site": Quotes/Config Items/Backup Status/Ticket Digest/Admin Sync each with ExternalLink | SATISFIED | DESKTOP_LINKS has all 5 entries; section 2 JSX renders ExternalLink after each label |
| DRAWER-05 | 02-01 | "Account": user (avatar + email read-only) + Sign out → signOut() then /auth/sign-in | SATISFIED | useSession provides user data; handleSignOut calls await signOut() then router.push('/auth/sign-in') with try/catch + toast |
| DRAWER-06 | 02-02 | app/mobile/nav/page.tsx deleted in same change | SATISFIED | File does not exist; git log shows commit 2af7395 deleted it; zero /mobile/nav references in codebase |
All 15 requirements: SATISFIED
Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
|---|---|---|---|---|
app/mobile/analyzer/page.tsx |
25 | "Analyzer feed coming soon" — deliberate placeholder | Info | Intentional stub. Documented in CONTEXT.md deferred section and 02-01-SUMMARY.md. Phase 6 (ANL-01..06) owns replacement. Does not block Phase 2 goal. |
components/mobile/HeaderBar.tsx |
50-51 | Bell onClick is empty {} |
Info | Intentional placeholder per SHELL-03. Phase 7+ (NOTIF-01) wires real notifications. Documented as deferred in REQUIREMENTS.md v2. |
No blockers. No warnings. Both items are explicitly documented as intentional deferred stubs.
Human Verification Required
1. WulfMark brand image renders correctly
Test: Open /mobile/dashboard in DevTools mobile view (390px). Confirm the Wulf "W" mark image renders visibly next to the "Pulse" wordmark in the sticky header — not a broken image icon or empty space.
Expected: The Wulf mark logo (PNG/SVG from components/branding/wulf-mark.tsx) appears as an image on the left of the header bar, with the "Pulse" text beside it.
Why human: WulfMark renders via a Next.js Image component with a file path — image availability and rendering cannot be confirmed by static code analysis.
2. Drawer slides in from the right; both triggers open the same drawer
Test: Visit any /mobile/* page. Tap the avatar circle in the top-right header. Observe the drawer animation and side. Close it. Tap "More" in the bottom nav. Confirm the same drawer opens again.
Expected: Sheet animates in from the right edge. No double-drawer flicker. Both entry points control the same drawer state.
Why human: Radix Sheet animation and single-instance behavior requires runtime DOM observation.
3. Drawer sections render in correct visual order with correct ExternalLink placement
Test: Open the More drawer. Visually confirm: (a) "Mobile sections" header above Engagement row — Engagement has NO external link icon; (b) "Full site" header above 5 rows, each with ExternalLink icon on the far right; (c) "Account" section at the bottom with signed-in user's name/email and red Sign out button. Expected: Three visually distinct sections. Engagement row clean (no hint icon). Full site rows each have the ExternalLink icon. User identity shows real session data. Why human: User identity requires a live session. Visual section separation and icon placement require rendering confirmation.
4. Sign out completes the full auth flow
Test: While signed in, open the drawer and tap "Sign out". Observe: (a) page navigates to /auth/sign-in; (b) refreshing the browser returns to sign-in, not an authenticated page.
Expected: Better Auth session is terminated server-side. No residual session cookie causes auto-redirect to /mobile. No error toast appears.
Why human: Auth session termination requires a live Better Auth backend call and cookie inspection.
5. Nested-route active tab highlighting
Test: Navigate to a ticket detail page (e.g. /mobile/tickets/123). Observe the bottom nav.
Expected: The "Tickets" tab shows text-primary color. All other tabs remain text-muted-foreground.
Why human: pathname.startsWith behavior on nested routes requires runtime navigation to confirm the correct tab lights up.
6. Content does not hide under bottom nav (SHELL-05)
Test: On /mobile/dashboard or any content-rich mobile page, scroll to the very bottom. Also test on a device/emulation with a home indicator (iPhone preset in DevTools).
Expected: Last content item is fully visible above the bottom nav. On home-indicator devices, the nav bar has visible padding below the "More" button text (the pb-safe utility reserves the inset space).
Why human: env(safe-area-inset-bottom) is device-dependent. The CSS calc expression cannot be validated without a rendered viewport.
7. /mobile/nav returns 404 (not old UI)
Test: Visit http://localhost:3100/mobile/nav directly in a browser.
Expected: Next.js default 404 page. The old standalone navigation UI (list of links) does not appear.
Why human: Requires running dev/prod server to confirm Next.js routing response.
Gaps Summary
No gaps. All 15 phase requirements are satisfied with substantive, wired implementations. The two documented stubs (analyzer placeholder, Bell empty onClick) are explicitly deferred by the spec and REQUIREMENTS.md and do not block the Phase 2 goal.
7 items require human verification — primarily visual/runtime behaviors that cannot be confirmed by static analysis (image rendering, animation, auth session termination, safe-area CSS, active tab color).
Verified: 2026-05-03T20:30:00Z Verifier: Claude (gsd-verifier)