wulf-pulse/.planning/phases/07-engagement-overview-new/07-03-SUMMARY.md
lorentz 0af6136864 docs(07-03): complete mobile engagement page plan summary
- Documents orchestration model (3 fetches on period, 1 on sort, 0 on search)
- Documents deviation: lowercase EngagementSortKey values vs plan template (Wave 2 actual)
- Documents field mapping: existing endpoint 'email' -> component 'userEmail'
- Confirms DRAWER-03 reachability and ENG-09 BottomNav unchanged
- Flags inherited risk T-07-12 (IDOR on /api/engagement/users) for follow-up
2026-05-03 22:58:41 -04:00

8.4 KiB
Raw Blame History

phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
07-engagement-overview-new 03 mobile-pages
mobile
engagement
page
orchestration
typescript
tailwind
requires provides affects
app/api/mobile/engagement/summary/route.ts (Plan 01 — MobileEngagementSummary type + endpoint)
app/api/mobile/engagement/trend/route.ts (Plan 01 — EngagementTrendResponse + SparklinePoint types)
app/api/engagement/users/route.ts (existing — reused as-is per D-16)
components/mobile/EngagementPeriodChips.tsx (Plan 02)
components/mobile/EngagementSummaryCard.tsx (Plan 02)
components/mobile/EngagementHoursSparkline.tsx (Plan 02)
components/mobile/EngagementSortChips.tsx (Plan 02)
components/mobile/EngagementSearchInput.tsx (Plan 02)
components/mobile/EngagementUserRow.tsx (Plan 02)
components/mobile/EngagementUserRowSkeleton.tsx (Plan 02)
components/mobile/MoreDrawer.tsx (Phase 2 — DRAWER-03 wires the route)
app/mobile/engagement/page.tsx
Phase 8 user profile page (app/mobile/engagement/[userId]) — tap target wired here
added patterns
'use client' + useState + useCallback + useEffect + useMemo + useRef (no SWR/react-query)
Three independent parallel fetches on mount/period change (summary + trend + users)
Single users refetch on sort change only
Client-side filter via useMemo (search never triggers a fetch)
IntersectionObserver infinite scroll (rootMargin 200px) + Load more fallback button
toast.error per failing fetch (sonner)
Skeleton loading states (4 cards + 1 sparkline + 5 rows)
prop mapping
existing API 'email' field mapped to EngagementUserRowData 'userEmail'
created modified
app/mobile/engagement/page.tsx
EngagementSortKey values are lowercase ('hours'|'name'|'utilization') per Wave 2 component — SORT_TO_API map keyed accordingly
EngagementSortChips uses {value, onChange} props (not {activeSort, onSortChange}) — matched actual component export
Existing /api/engagement/users returns 'email' field; EngagementUserRowData expects 'userEmail' — mapped inline at render
Default sort set to 'hours' (lowercase) matching EngagementSortKey type constraint
Period/sort/search state held in component state only (no URL sync) per D-21 note and lower data scale vs Phase 4 tickets
duration completed tasks files_created files_modified
~15 min 2026-05-04 1 1 0

Phase 7 Plan 03: Mobile Engagement Page (Wave 3) Summary

Mobile Engagement overview page wiring all Plan 01 endpoints + Plan 02 components into a single 'use client' page with 3-fetch orchestration, infinite scroll, and complete loading/empty/error states.

What Was Built

app/mobile/engagement/page.tsx (commit 5daf7f3, 378 lines)

Phone-first refactor of the Engagement overview (ENG-01 — real refactor, not a desktop port).

Orchestration model:

Trigger Fetches fired
Mount (period = D30 default) summary + trend + users page 1 (parallel)
Period chip tap summary + trend + users page 1 (parallel)
Sort chip tap users page 1 only
Search input change 0 fetches (client-side useMemo filter)
IntersectionObserver / Load more users page N+1

Page layout order (per UI-SPEC):

  1. <h1 className="text-sm font-semibold">Engagement</h1> — scrolls away under sticky chips
  2. <EngagementPeriodChips period={period} onPeriodChange={setPeriod} /> — sticky strip (D-05)
  3. 4× <EngagementSummaryCard> stacked (space-y-3): Active users, Total Graph hours, Total Autotask hours, Hours / active user
  4. <EngagementHoursSparkline points={trendPoints} period={period} /> — compact sparkline card
  5. Sort + search controls (space-y-2): <EngagementSortChips> then <EngagementSearchInput>
  6. User list (divide-y border rounded-xl overflow-hidden)
  7. Sentinel + Load more button

States wired:

State Trigger What renders
Initial loading mount 4 summary card skeletons + sparkline skeleton + 5 user-row skeletons
Not-configured summary.configured === false Banner replaces sections 37
Empty activeUsers === 0 AND users.length === 0 Empty state card with period chips still interactive
No-matches search filter → 0 results Inline "No matches for {query}" + Clear search button
Load more in-flight intersection fires Loader2 spinner; button disabled
Load more error fetch throws toast.error + button flips to "Retry"

Fetch error toasts (D-25, all 4 present):

  • toast.error('Failed to load engagement summary')
  • toast.error('Failed to load hours trend')
  • toast.error('Failed to load engagement users')
  • toast.error('Failed to load more team members')

Reachability Confirmed

  • DRAWER-03: components/mobile/MoreDrawer.tsx routes to /mobile/engagement — verified, not modified.
  • ENG-09: components/mobile/BottomNav.tsx has no Engagement entry — verified, not modified.

Engagement is exclusively reachable from the More drawer. Bottom nav unchanged (4 tabs + More cell).

Inherited Risk: T-07-12 (IDOR on /api/engagement/users)

The existing /api/engagement/users endpoint reused by this page (per D-16, D-33) does not call requireAuth(). This is an existing-product gap. The new mobile page does NOT introduce new exposure: middleware.ts requires session for all /api/* routes not in the public list, and /api/engagement/users is not in the public list.

Recommended follow-up: A future security phase should retrofit requireAuth() onto all desktop engagement endpoints (/api/engagement/*). Track in STATE.md.

Key Deviation: Sort Key Casing

The plan's interface templates used capitalized EngagementSortKey values ('Hours'|'Name'|'Utilization') but the Wave 2 EngagementSortChips component (Plan 02) exports lowercase values ('hours'|'name'|'utilization'). This was caught by reading the actual component files before writing the page. Adjustments made:

  • SORT_TO_API map keyed on lowercase values
  • Default sortKey state set to 'hours' (not 'Hours')
  • EngagementSortChips called with value={sortKey} and onChange={setSortKey} (matching actual prop names, not the plan template's activeSort/onSortChange)

These are Rule 1 (auto-fix) adjustments — aligning the page to what the actual component exports.

Key Deviation: userEmail Field Mapping

The existing /api/engagement/users endpoint returns email field per its response shape. EngagementUserRowData (Wave 2 type) expects userEmail. The page performs the mapping inline at render:

user={{
  graphUserId: u.graphUserId,
  displayName: u.displayName,
  userEmail: u.email,   // 'email' from existing endpoint → 'userEmail' expected by component
  jobTitle: u.jobTitle,
  billableHours: u.billableHours,
  hoursWorked: u.hoursWorked,
}}

This is the correct approach — the existing endpoint is unchanged (D-34) and the component contract is respected.

No Stubs

The page is fully wired to all 3 data sources. No hardcoded mock data, no placeholder values beyond loading skeletons (which are correct UX, not stubs).

Threat Flags

None new in this file. All user-supplied strings (search query, displayName, email, jobTitle) are rendered via React JSX text interpolation (auto-escaped). The search query appears in copy via template literal — React escapes the closing tags. No dangerouslySetInnerHTML. The graphUserId in Link href comes from API response (not direct user input).

T-07-12 (IDOR on /api/engagement/users) is a pre-existing risk documented in Plan 01 SUMMARY; this plan inherits it without introducing new exposure.

Self-Check: PASSED

All checks passed:

  • app/mobile/engagement/page.tsx exists (378 lines, above 200 minimum)
  • Commit 5daf7f3 confirmed
  • npx tsc --noEmit --pretty exits 0
  • 'use client'; at top of file
  • All 7 Wave-2 components imported
  • Type imports from both Plan 01 endpoint files
  • H1 uses text-sm font-semibold (not text-base)
  • No font-medium in file
  • BottomNav.tsx has no Engagement entry (ENG-09 — verified, unmodified)
  • MoreDrawer.tsx routes to /mobile/engagement (DRAWER-03 — verified, unmodified)
  • No Zod, SWR, or react-query imports
  • No useSearchParams or useRouter
  • All 4 toast.error messages present verbatim
  • All copy strings from UI-SPEC Copywriting Contract present