diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 200f958..0964bef 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -131,9 +131,9 @@ Decimal phases appear between their surrounding integers in numeric order. 4. The per-employee list renders as stacked rows (avatar/initials, name, role, hours bar) with a search input and a sort control above (sort by hours, name, utilization) 5. A single compact "hours trend" sparkline renders at the top of the list, scoped to the selected period — no multi-series chart **Plans**: 3 plans -- [ ] 07-01-PLAN.md — /api/mobile/engagement/summary + /api/mobile/engagement/trend endpoints with period whitelist + requireAuth (ENG-03, ENG-05) -- [ ] 07-02-PLAN.md — Engagement* mobile components (PeriodChips, SummaryCard, HoursSparkline, SortChips, SearchInput, UserRow, UserRowSkeleton + getInitials utility) (ENG-02, ENG-03, ENG-04, ENG-05) -- [ ] 07-03-PLAN.md — app/mobile/engagement/page.tsx orchestration (period/sort state, IntersectionObserver, empty/error/not-configured states) (ENG-01, ENG-02, ENG-03, ENG-04, ENG-05, ENG-09) +- [x] 07-01-PLAN.md — /api/mobile/engagement/summary + /api/mobile/engagement/trend endpoints with period whitelist + requireAuth (ENG-03, ENG-05) +- [x] 07-02-PLAN.md — Engagement* mobile components (PeriodChips, SummaryCard, HoursSparkline, SortChips, SearchInput, UserRow, UserRowSkeleton + getInitials utility) (ENG-02, ENG-03, ENG-04, ENG-05) +- [x] 07-03-PLAN.md — app/mobile/engagement/page.tsx orchestration (period/sort state, IntersectionObserver, empty/error/not-configured states) (ENG-01, ENG-02, ENG-03, ENG-04, ENG-05, ENG-09) **UI hint**: yes ### Phase 8: Engagement User Profile (NEW) diff --git a/.planning/STATE.md b/.planning/STATE.md index fb4aa62..1d9e168 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,14 +4,14 @@ milestone: v1.0 milestone_name: milestone status: executing stopped_at: Phase 7 UI-SPEC approved -last_updated: "2026-05-04T02:40:53.292Z" -last_activity: 2026-05-04 -- Phase 7 planning complete +last_updated: "2026-05-04T03:04:12.504Z" +last_activity: 2026-05-04 progress: total_phases: 8 - completed_phases: 6 + completed_phases: 7 total_plans: 17 - completed_plans: 14 - percent: 82 + completed_plans: 17 + percent: 100 --- # Project State @@ -21,14 +21,14 @@ progress: See: .planning/PROJECT.md (updated 2026-05-03) **Core value:** A manager can open Pulse on their phone and, in under 30 seconds, see the state of the business and triage tickets — without ever needing to switch to desktop for read-only awareness. -**Current focus:** Phase 06 — Analyzer Feed (NEW) +**Current focus:** Phase 07 — Engagement Overview (NEW) ## Current Position -Phase: 7 +Phase: 8 Plan: Not started -Status: Ready to execute -Last activity: 2026-05-04 -- Phase 7 planning complete +Status: Executing Phase 07 +Last activity: 2026-05-04 Progress: [░░░░░░░░░░] 0% @@ -36,7 +36,7 @@ Progress: [░░░░░░░░░░] 0% **Velocity:** -- Total plans completed: 14 +- Total plans completed: 17 - Average duration: — - Total execution time: 0.0 hours @@ -50,6 +50,7 @@ Progress: [░░░░░░░░░░] 0% | 04 | 3 | - | - | | 05 | 2 | - | - | | 06 | 3 | - | - | +| 07 | 3 | - | - | **Recent Trend:** diff --git a/.planning/phases/07-engagement-overview-new/07-VERIFICATION.md b/.planning/phases/07-engagement-overview-new/07-VERIFICATION.md new file mode 100644 index 0000000..e3a6d69 --- /dev/null +++ b/.planning/phases/07-engagement-overview-new/07-VERIFICATION.md @@ -0,0 +1,202 @@ +--- +phase: 07-engagement-overview-new +verified: 2026-05-04T12:00:00Z +status: human_needed +score: 5/5 must-haves verified +human_verification: + - test: "Open the app on a phone (or mobile viewport), tap More drawer, tap 'Engagement' — verify the page loads at /mobile/engagement inside the Phase 2 shell with HeaderBar and BottomNav visible." + expected: "The Engagement page renders with H1 'Engagement', sticky 3-chip period selector (7d/30d/90d), 4 stacked summary cards, a sparkline card, sort chips, search input, and a list of employee rows or skeleton placeholders." + why_human: "Navigation tap + visual rendering of the full composed page cannot be verified programmatically without a running server and browser." + - test: "With the page loaded, tap each period chip (7d, 30d, 90d) and observe network activity." + expected: "Each period tap fires 3 fetch requests (summary + trend + users), the active chip style changes to bg-primary, and all cards/sparkline update with new data for that period." + why_human: "Requires observing network panel and visual active state transitions — cannot be verified by static analysis." + - test: "Tap the 'Hours' / 'Name' / 'Utilization' sort chips." + expected: "Each sort chip tap fires only 1 fetch (users only; summary and trend do NOT refetch). The user list re-orders accordingly." + why_human: "Requires observing network requests during sort chip interaction — live browser testing only." + - test: "Type in the search input and observe filtering." + expected: "No fetch fires while typing. After 300ms of no keystrokes, the visible user list filters by displayName and email. If the filter zeroes out results, 'No matches for ...' renders with a 'Clear search' button. Clearing resets to full list." + why_human: "Debounce behavior and client-side filter require browser interaction with real data." + - test: "With a team that has more than 50 staff, scroll to bottom of the user list." + expected: "When the sentinel div enters viewport (200px before bottom), a new page of users loads automatically. The 'Load more' button is also present and works as a fallback. Reaching the last page hides the Load more button." + why_human: "Requires real data with pagination and browser scroll observation." + - test: "Verify the sparkline renders correctly with data." + expected: "A thin primary-colored line appears across the 48px SVG area. A faint baseline is visible. The label row shows 'Hours trend · last 30 days' (or appropriate period) on the left and the latest value (e.g. '12.4h today') on the right. No dots, axes, or tooltips appear." + why_human: "SVG rendering and visual correctness of the sparkline path require visual inspection." + - test: "Tap a user row." + expected: "Navigation goes to /mobile/engagement/[graphUserId]. The Phase 8 page does not exist yet, so a 404 is expected — this validates the Link href is correctly wired with the graphUserId." + why_human: "Link navigation requires browser interaction to confirm the URL resolves correctly." +--- + +# Phase 7: Engagement Overview (NEW) Verification Report + +**Phase Goal:** A manager reaches Engagement from the More drawer and sees a phone-first overview — period chips, stacked summary cards, a sortable per-employee list, and one compact sparkline. +**Verified:** 2026-05-04T12:00:00Z +**Status:** human_needed +**Re-verification:** No — initial verification + +## Execution Incident (Recorded for Transparency) + +Wave 2 worktree-base recovery: The first attempted merge of Wave 2's executor worktree would have deleted multiple prior-phase files. Investigation revealed the worktree branch was created from a stale base. Resolution: orchestrator cherry-picked the 7 new Engagement* component files into the parent branch via a recovery commit (d637892). All 7 components verified present; all prior-phase files intact (21 components total in `components/mobile/`). TypeScript exits 0. + +This incident does NOT affect the verdict — the recovered state is identical to what an in-place merge would have produced. + +## Documented Deviations (Pre-approved) + +1. **Period chips (ENG-02):** Roadmap SC-2 specifies "today / 7d / 30d" but data layer only supports D7/D30/D90. Phase 7 uses `7d / 30d / 90d` chips (CONTEXT.md D-07). "Today" deferred to a future phase that adds D1 sync. +2. **EngagementSortKey casing:** Plan templates used `'Hours'|'Name'|'Utilization'` but component exports `'hours'|'name'|'utilization'` (lowercase). The page's `SORT_TO_API` map uses matching lowercase keys. Display labels are correctly capitalized in the chip UI. +3. **Page H1 typography:** Uses `text-sm font-semibold` (not `text-base`) per UI-SPEC fix to keep declared font sizes at 4. +4. **Inherited security gap on `/api/engagement/users`:** Existing endpoint lacks explicit `requireAuth()`. Not made worse by Phase 7; documented for STATE.md follow-up. + +## Goal Achievement + +### Observable Truths + +| # | Truth | Status | Evidence | +|----|-------|--------|----------| +| 1 | More drawer links to `/mobile/engagement`; Engagement is NOT on the bottom nav (ENG-09) | VERIFIED | `MoreDrawer.tsx:37` has `{ href: '/mobile/engagement', label: 'Engagement', icon: Users }`. `BottomNav.tsx` has 0 occurrences of "Engagement"/"engagement". | +| 2 | Page shows sticky 3-chip period selector (7d/30d/90d, default 30d) with active state | VERIFIED | `EngagementPeriodChips.tsx` has `sticky top-0 z-10 bg-background` container, chips D7/D30/D90, `aria-pressed`, active/inactive class strings. `page.tsx:64` defaults to `'D30'`. | +| 3 | 4 summary cards stacked single-column (active users, total Graph hours, total Autotask hours, hours/active user) | VERIFIED | `EngagementSummaryCard.tsx` exists with `text-2xl font-semibold` big number + `text-xs` label. `page.tsx:266-270` renders 4 instances in `space-y-3`. `/api/mobile/engagement/summary` returns all 4 totals from real DB queries. | +| 4 | Per-employee list renders sortable + searchable stacked rows (avatar/name/role/hours bar) with navigation link | VERIFIED | `EngagementUserRow.tsx` has Link to `/mobile/engagement/${user.graphUserId}`, avatar initials, `text-sm font-semibold` name, role conditional, `h-1.5 bg-muted` hours bar with `bg-primary` fill. `EngagementSortChips.tsx` + `EngagementSearchInput.tsx` wired in page. | +| 5 | Compact "hours trend" sparkline (no multi-series chart) renders at top of list, scoped to period | VERIFIED | `EngagementHoursSparkline.tsx` uses inline SVG only (no recharts), imports `SparklinePoint` from Plan 01 route, `h-12 w-full` SVG. `/api/mobile/engagement/trend` returns daily points via `generate_series`. Page passes `points={trendPoints} period={period}`. | + +**Score:** 5/5 truths verified + +### Required Artifacts + +| Artifact | Expected | Status | Details | +|----------|----------|--------|---------| +| `app/api/mobile/engagement/summary/route.ts` | Mobile summary endpoint (4 totals + configured flag) | VERIFIED | 152 lines. Exports `MobileEngagementSummary`, `GET`. `requireAuth()` first. Period whitelist + 400. Real DB queries (3 SQL queries). `isMsgraphConfigured()` called. No Zod. | +| `app/api/mobile/engagement/trend/route.ts` | Daily hours trend endpoint | VERIFIED | 92 lines. Exports `SparklinePoint`, `EngagementTrendResponse`, `GET`. `requireAuth()` first. `generate_series` for continuous daily series. Period whitelist + 400. No Zod. | +| `components/mobile/EngagementPeriodChips.tsx` | 3-chip period selector | VERIFIED | 45 lines. `'use client'`. Exports `EngagementPeriodChips`, `EngagementPeriod`, `EngagementPeriodChipsProps`. Sticky container, `aria-pressed`, active/inactive class strings. | +| `components/mobile/EngagementSummaryCard.tsx` | Single summary card primitive | VERIFIED | 25 lines. `'use client'`. Exports `EngagementSummaryCard`. `text-2xl font-semibold`, `text-xs text-muted-foreground`, `shadow-none`. | +| `components/mobile/EngagementHoursSparkline.tsx` | Custom SVG sparkline | VERIFIED | 105 lines. `'use client'`. Exports `EngagementHoursSparkline`. Inline SVG, `stroke-primary`, `h-12 w-full`, no recharts, `SparklinePoint` type from Plan 01. "No activity" fallback. | +| `components/mobile/EngagementSortChips.tsx` | 3-chip sort selector | VERIFIED | 45 lines. `'use client'`. Exports `EngagementSortChips`, `EngagementSortKey` (lowercase values). `aria-pressed` on each chip. | +| `components/mobile/EngagementSearchInput.tsx` | Debounced search input | VERIFIED | 49 lines. `'use client'`. Exports `EngagementSearchInput`. 300ms debounce via `setTimeout`. `aria-label`, `Search` icon, shadcn `Input`. | +| `components/mobile/EngagementUserRow.tsx` | User row + getInitials | VERIFIED | 87 lines. `'use client'`. Exports `EngagementUserRow`, `getInitials`, `EngagementUserRowData`, `EngagementUserRowProps`. Link to `/mobile/engagement/${user.graphUserId}`, hours bar, avatar initials, conditional role. | +| `components/mobile/EngagementUserRowSkeleton.tsx` | Skeleton matching row shape | VERIFIED | 23 lines. `'use client'`. Exports `EngagementUserRowSkeleton`. Mirrors row: avatar circle, name, hours, role, bar. | +| `app/mobile/engagement/page.tsx` | Mobile engagement overview page | VERIFIED | 378 lines (well above 200 minimum). `'use client'`. Default export `MobileEngagementPage`. All 7 Wave-2 components imported. All 3 endpoints called. IntersectionObserver. All states wired. | + +### Key Link Verification + +| From | To | Via | Status | Details | +|------|----|-----|--------|---------| +| `summary/route.ts` | `lib/auth-utils.ts` | `requireAuth()` first in handler | WIRED | Line 23: `const { error: authError } = await requireAuth()` — occurs before any `postgresClient.query` call | +| `summary/route.ts` | `lib/services/msgraph-factory.ts` | `isMsgraphConfigured()` | WIRED | Line 4 import, called at lines 45 and 135 in response | +| `summary/route.ts` | `engagement_snapshots / time_entries` | `postgresClient.query` | WIRED | 3 SQL queries at lines 37, 71, 92, 117 against real tables | +| `trend/route.ts` | `time_entries` | `generate_series` + LEFT JOIN | WIRED | SQL query at line 45 uses `time_entries` with `generate_series` for continuous series | +| `EngagementHoursSparkline.tsx` | `SparklinePoint` type | `import type from '@/app/api/mobile/engagement/trend/route'` | WIRED | Line 10: `import type { SparklinePoint } from '@/app/api/mobile/engagement/trend/route'` | +| `EngagementUserRow.tsx` | `/mobile/engagement/[graphUserId]` | `next/link href` template literal | WIRED | Line 48: `href={\`/mobile/engagement/${user.graphUserId}\`}` | +| `EngagementSummaryCard.tsx` | shadcn Card | `@/components/ui/card` | WIRED | Line 9: `import { Card, CardContent } from '@/components/ui/card'` | +| `page.tsx` | `/api/mobile/engagement/summary` | `fetch` in `useCallback` | WIRED | Line 85: `fetch(\`/api/mobile/engagement/summary?period=${p}\`)` | +| `page.tsx` | `/api/mobile/engagement/trend` | `fetch` in `useCallback` | WIRED | Line 100: `fetch(\`/api/mobile/engagement/trend?period=${p}\`)` | +| `page.tsx` | `/api/engagement/users` | `fetch` with period+sort+page params | WIRED | Line 118: `fetch(\`/api/engagement/users?${sp.toString()}\`)` | +| `page.tsx` | All 7 Engagement* components | named imports from `@/components/mobile/Engagement*` | WIRED | Lines 16-22: 7 component imports, all rendered in JSX | +| `page.tsx` | `MobileEngagementSummary` type | `import type from '@/app/api/mobile/engagement/summary/route'` | WIRED | Line 14 | + +### Data-Flow Trace (Level 4) + +| Artifact | Data Variable | Source | Produces Real Data | Status | +|----------|---------------|--------|--------------------|--------| +| `app/mobile/engagement/page.tsx` | `summary` | `fetch /api/mobile/engagement/summary` → `setSummary(data)` | Yes — 3 SQL queries against `engagement_snapshots`, `graph_users`, `resources`, `time_entries` | FLOWING | +| `app/mobile/engagement/page.tsx` | `trendPoints` | `fetch /api/mobile/engagement/trend` → `setTrendPoints(data.points)` | Yes — `generate_series` + `time_entries` daily aggregate | FLOWING | +| `app/mobile/engagement/page.tsx` | `users` | `fetch /api/engagement/users` → `setUsers(data.users)` | Yes — existing endpoint queries DB (reused as-is) | FLOWING | +| `EngagementHoursSparkline.tsx` | `points` prop | Passed from `page.tsx:285` as `trendPoints` | Yes — flows from trend endpoint | FLOWING | +| `EngagementSummaryCard.tsx` | `value`, `label` props | Passed from `page.tsx:266-270` with formatted summary values | Yes — flows from summary endpoint | FLOWING | +| `EngagementUserRow.tsx` | `user`, `maxHours` props | Passed from `page.tsx:335-346` per `filteredUsers` | Yes — flows from users endpoint | FLOWING | + +### Behavioral Spot-Checks + +Step 7b: SKIPPED — Next.js route handlers require a running server. All key behaviors were verified via static analysis of the fetch wiring, data transforms, and state management patterns. + +### Requirements Coverage + +| Requirement | Source Plan | Description | Status | Evidence | +|-------------|-------------|-------------|--------|----------| +| ENG-01 | 07-03 | `/mobile/engagement` overview page — real refactor, not desktop port | SATISFIED | `app/mobile/engagement/page.tsx`, 378 lines, purpose-built phone-first (not ported from 1300-line desktop page) | +| ENG-02 | 07-02, 07-03 | Period selector chip row sticky just below page H1 | SATISFIED | `EngagementPeriodChips.tsx` (sticky strip). Deviation D-07: uses 7d/30d/90d instead of today/7d/30d — data layer constraint, documented and approved | +| ENG-03 | 07-01, 07-02, 07-03 | Summary cards stacked single-column (active users, Graph hours, AT hours, hours/user) | SATISFIED | All 4 cards via `EngagementSummaryCard`; totals from `/api/mobile/engagement/summary` | +| ENG-04 | 07-02, 07-03 | Per-employee list with sort control and search input | SATISFIED | `EngagementUserRow`, `EngagementSortChips`, `EngagementSearchInput`; sort chips wired to API; search debounced 300ms client-side | +| ENG-05 | 07-01, 07-02, 07-03 | Compact hours trend sparkline at top of list, scoped to period | SATISFIED | `EngagementHoursSparkline` (inline SVG, no recharts); `/api/mobile/engagement/trend` returns daily points via `generate_series` | +| ENG-09 | 07-03 | Engagement reachable from More drawer, NOT bottom bar | SATISFIED | `MoreDrawer.tsx:37` has Engagement entry; `BottomNav.tsx` has 0 occurrences | + +No orphaned requirements: REQUIREMENTS.md maps exactly ENG-01, ENG-02, ENG-03, ENG-04, ENG-05, ENG-09 to Phase 7. All 6 are accounted for across Plans 01/02/03. + +### Anti-Patterns Found + +No anti-patterns detected. Scan covered all 10 Phase 7 files for TODO/FIXME/PLACEHOLDER comments, empty implementations, and hardcoded stub values. Zero matches found. + +| File | Pattern | Severity | Impact | +|------|---------|----------|--------| +| (none) | — | — | — | + +### Human Verification Required + +All 5 roadmap success criteria pass automated verification. The following items require human testing in a running browser environment: + +**1. Drawer-to-page navigation** + +**Test:** Open the app on a mobile viewport. Tap the "More" cell in the bottom nav. Tap "Engagement" in the Mobile sections row. +**Expected:** Navigation lands at `/mobile/engagement` inside the Phase 2 shell. Page renders H1 "Engagement", sticky 3-chip period selector, 4 summary card skeletons (then real values), sparkline skeleton (then chart), sort chips, search input, and employee row skeletons (then rows). +**Why human:** Drawer tap interaction, shell mounting, and initial loading state sequence require a running browser. + +**2. Period chip refetch behavior** + +**Test:** With data loaded, tap each of the three period chips (7d, 30d, 90d). Observe browser network panel. +**Expected:** Each tap fires exactly 3 requests (summary + trend + users). The active chip changes to `bg-primary`. All data updates. +**Why human:** Network request timing and visual active-state transitions require a running browser. + +**3. Sort chip single-fetch behavior** + +**Test:** Tap each sort chip. Observe network panel. +**Expected:** Each sort chip tap fires exactly 1 request (users only). Summary and trend do NOT refetch. +**Why human:** Requires distinguishing between fetch calls in a real browser. + +**4. Search debounce and no-matches state** + +**Test:** Type in the search input. Observe: (a) no fetch fires while typing; (b) list filters after 300ms pause; (c) if filter yields zero results, "No matches for ..." renders with "Clear search" button; (d) clearing the input restores the full list. +**Expected:** Behavior as described. Client-side only. +**Why human:** Debounce timing and filter state transitions require browser interaction with real data. + +**5. Sparkline visual rendering** + +**Test:** With data loaded, visually inspect the sparkline card. +**Expected:** A thin primary-color line spanning the 48px SVG area. A faint baseline near the bottom. Label row: "Hours trend · last 30 days" (left) and "X.Xh today" or date-formatted value (right). No dots, axes, or tooltips. +**Why human:** SVG path rendering and visual appearance require browser rendering. + +**6. Infinite scroll and Load more** + +**Test:** On a team with >50 staff, scroll to the bottom of the employee list. +**Expected:** When the sentinel enters the viewport (200px before bottom), a new page loads automatically. The "Load more" button also works as a fallback. When the last page is reached, the button hides. +**Why human:** Requires real paginated data and browser scroll observation. + +**7. User row navigation** + +**Test:** Tap a user row. +**Expected:** Browser navigates to `/mobile/engagement/[graphUserId]`. A 404 is expected (Phase 8 not built yet) — but the URL must contain the correct graphUserId, confirming the link is wired correctly. +**Why human:** Link navigation requires a browser. + +### Gaps Summary + +No gaps. All automated checks pass: +- All 10 artifacts exist, are substantive (no stubs), and are fully wired +- All key links verified (auth gates, DB queries, component imports, fetch calls) +- Data flows from DB through API endpoints to page state to components +- TypeScript exits 0 (`npx tsc --noEmit --pretty`) +- Zero anti-patterns (no TODO/FIXME/placeholder/empty-implementation patterns) +- All 6 requirements (ENG-01..05, ENG-09) have implementation evidence +- Approved deviation D-07 (7d/30d/90d instead of today/7d/30d) is documented in CONTEXT.md — not a gap + +Awaiting human verification of 7 behavioral/visual items above. + +--- + +## Inherited Issues (Not Counted Against Verdict) + +**Pre-existing test failures (2 tests in `lib/services/analyzer/itglue-search.test.ts`):** These failures predate Phase 7. Phase 7 modified zero files in `lib/services/analyzer/`. Test suite otherwise passes 182/184 = 98.9%. + +**Inherited security gap (T-07-05/T-07-12):** Existing `/api/engagement/users` endpoint lacks `requireAuth()`. Phase 7 reuses this endpoint as-is per D-16/D-34. Not made worse by this phase. Recommended for a future security phase. + +--- + +_Verified: 2026-05-04T12:00:00Z_ +_Verifier: Claude (gsd-verifier)_