docs(phase-08): complete phase execution

This commit is contained in:
lorentz 2026-05-08 09:58:32 -04:00
parent 25f03cd9d1
commit a5e43fddbf
3 changed files with 126 additions and 12 deletions

View file

@ -165,8 +165,8 @@ Decimal phases appear between their surrounding integers in numeric order.
2. The profile is a real page (not a modal) — the device/browser back gesture returns to the overview at the same scroll position
3. The profile renders single-column: identity header → period selector → key metrics (compact) → activity breakdown list → recent items, sourced from the existing engagement profile data endpoints (no new data)
**Plans**: 2 plans
- [ ] 08-01-PLAN.md — MS Graph user-photo proxy at /api/mobile/engagement/user/[userId]/photo (ENG-06; D-25, D-26)
- [ ] 08-02-PLAN.md — Mobile profile page at /mobile/engagement/[userId] + 6 EngagementProfile* components (ENG-06, ENG-07, ENG-08)
- [x] 08-01-PLAN.md — MS Graph user-photo proxy at /api/mobile/engagement/user/[userId]/photo (ENG-06; D-25, D-26)
- [x] 08-02-PLAN.md — Mobile profile page at /mobile/engagement/[userId] + 6 EngagementProfile* components (ENG-06, ENG-07, ENG-08)
**UI hint**: yes
### Phase 9: User Profile & Preferences (NEW)

View file

@ -4,14 +4,14 @@ milestone: v1.0
milestone_name: milestone
status: executing
stopped_at: Phase 8 UI-SPEC approved
last_updated: "2026-05-07T23:45:22.929Z"
last_activity: 2026-05-07 -- Phase 08 planning complete
last_updated: "2026-05-08T13:58:28.740Z"
last_activity: 2026-05-08
progress:
total_phases: 10
completed_phases: 8
completed_phases: 9
total_plans: 24
completed_plans: 22
percent: 92
completed_plans: 24
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 07 — Engagement Overview (NEW)
**Current focus:** Phase 08 — engagement-user-profile-new
## Current Position
Phase: 8
Phase: 9
Plan: Not started
Status: Ready to execute
Last activity: 2026-05-07 -- Phase 08 planning complete
Status: Executing Phase 08
Last activity: 2026-05-08
Progress: [░░░░░░░░░░] 0%
@ -36,7 +36,7 @@ Progress: [░░░░░░░░░░] 0%
**Velocity:**
- Total plans completed: 22
- Total plans completed: 24
- Average duration: —
- Total execution time: 0.0 hours
@ -52,6 +52,7 @@ Progress: [░░░░░░░░░░] 0%
| 06 | 3 | - | - |
| 07 | 3 | - | - |
| 07.1 | 5 | - | - |
| 08 | 2 | - | - |
**Recent Trend:**

View file

@ -0,0 +1,113 @@
---
phase: 08-engagement-user-profile-new
verified: 2026-05-08T00:00:00Z
status: human_needed
score: 12/13 must-haves verified
re_verification: false
human_verification:
- test: "Navigate to /mobile/engagement, tap an employee row, press the device back button"
expected: "Returns to /mobile/engagement at the same scroll position as before tapping"
why_human: "Scroll restoration (SC#2) is programmatic — the sessionStorage shim was added to app/mobile/engagement/page.tsx but the SUMMARY documents it as partially working. Browser-back scroll position cannot be verified by grep or static analysis. User has accepted the partial behavior as a known limitation."
---
# Phase 8: Engagement User Profile (NEW) — Verification Report
**Phase Goal:** From the Engagement overview, a manager taps an employee row and arrives at a real, shareable profile page — single-column phone-first — and the device back gesture returns them to the overview.
**Verified:** 2026-05-08
**Status:** human_needed
**Re-verification:** No — initial verification
## Goal Achievement
### Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | Tapping a row in /mobile/engagement navigates to /mobile/engagement/{graphUserId} | ✓ VERIFIED | `EngagementUserRow.tsx:48` has `href={'/mobile/engagement/${user.graphUserId}'}` pointing to the real route at `app/mobile/engagement/[userId]/page.tsx` |
| 2 | The browser back gesture returns to the overview at the prior scroll position (SC#2) | ? HUMAN NEEDED | sessionStorage shim exists at `app/mobile/engagement/page.tsx:175-225` (rAF save + retry restore), but SUMMARY and context notes both document this as partial — does not reliably restore scroll. User accepted as known limitation. |
| 3 | The profile renders single-column in the specified order: H1 → period chips → identity header → 2×2 metric grid → activity breakdown → recent entries → recent meetings | ✓ VERIFIED | `page.tsx:183-224` confirms exact render order: `<h1>`, `<EngagementPeriodChips>`, `<EngagementProfileHeader>`, `<EngagementProfileMetricGrid>`, `<EngagementProfileBreakdown>`, `<EngagementRecentEntries>`, `<EngagementRecentMeetings>` |
| 4 | Identity header shows photo (or initials fallback), display name, jobTitle, department (omitted if null), email as mailto, last-active row when signal exists | ✓ VERIFIED | `EngagementProfileHeader.tsx:35-88``photoFailed` useState toggle, `onError={() => setPhotoFailed(true)}`, department conditional at line 78, `href={mailto:${email}}` at line 82, `lastActiveLabel &&` at line 87 |
| 5 | 2×2 hero metric cards (Hours worked / Billable hours / Days worked / Meetings attended) render in grid-cols-2 from /api/engagement/user/[userId]?period= | ✓ VERIFIED | `EngagementProfileMetricGrid.tsx:24` has `className="grid grid-cols-2 gap-3"` with all four metric labels at lines 28, 34, 40, 46; data flows from page.tsx via props |
| 6 | Period chip selection refetches data; recent-items remain bound to 10 regardless of period | ✓ VERIFIED | `page.tsx:117` — useEffect deps `[userId, period, retryNonce]`; fetch at line 93 uses `?period=${period}`; EngagementRecentEntries/Meetings components receive raw `recentEntries` / `recentTeamsMeetings` arrays not filtered by period |
| 7 | Activity breakdown renders Time / Communication / Meetings subsections with after-hours and Zoom conditional rows | ✓ VERIFIED | `EngagementProfileBreakdown.tsx:51-91` — three labeled `<h3>` subsections; `showAfterHours` at line 45 gates after-hours row; `showZoom` at line 46 gates Zoom row |
| 8 | Recent entries and meetings collapse/expand via shadcn Collapsible; Set<string> state; period changes don't reset expansion | ✓ VERIFIED | Both components use `Collapsible`, `CollapsibleTrigger`, `CollapsibleContent` from shadcn; `useState<Set<string>>(new Set())` for expansion tracking; period changes propagate to page, not through the component state |
| 9 | 404 from data endpoint renders inline "User not found" with back-to-Engagement link | ✓ VERIFIED | `page.tsx:120-132``errorState === 'not-found'` branch renders `<h1>User not found</h1>` and `<Link href="/mobile/engagement">Back to Engagement</Link>` |
| 10 | 500/network failure renders sonner toast + inline Retry button via retryNonce increment | ✓ VERIFIED | `page.tsx:5` imports `toast from 'sonner'`; `page.tsx:111` calls `toast.error()`; Retry button at line 143 calls `setRetryNonce((n) => n + 1)` |
| 11 | Photo endpoint non-200 silently falls back to initials — no toast, no error UI | ✓ VERIFIED | `EngagementProfileHeader.tsx:61``onError={() => setPhotoFailed(true)}` toggles the `photoFailed` state, rendering initials only; no toast import in that component |
| 12 | EngagementUserRow.tsx was NOT modified by this plan | ✓ VERIFIED | `git log components/mobile/EngagementUserRow.tsx` — last commit is `d637892` (Phase 7); no Phase 8 commits touch this file |
| 13 | /api/engagement/user/[userId]/route.ts was NOT modified by this plan | ✓ VERIFIED | `git log app/api/engagement/user/[userId]/route.ts` — last commit is `c518eef` (unrelated); no Phase 8 commits touch this file |
**Score:** 12/13 truths verified (1 requires human testing — SC#2 scroll restoration)
### Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `lib/services/msgraph-client.ts` | getUserPhotoBytes() method | ✓ VERIFIED | Line 419 — `async getUserPhotoBytes(userId)` exists; returns ArrayBuffer on 2xx, null on 404, throws on other non-2xx; uses `this.getToken()` |
| `app/api/mobile/engagement/user/[userId]/photo/route.ts` | Photo proxy GET handler | ✓ VERIFIED | 67 lines; exports `GET`; `requireAuth` is first call (line 10); all status codes present (400, 404, 503) |
| `app/mobile/engagement/[userId]/page.tsx` | Mobile profile page | ✓ VERIFIED | 230 lines (>= 120 minimum); `'use client'` at line 1; all 7 components wired |
| `components/mobile/EngagementProfileSkeleton.tsx` | Full-page skeleton | ✓ VERIFIED | 77 lines; exports `EngagementProfileSkeleton`; contains `Skeleton` |
| `components/mobile/EngagementProfileHeader.tsx` | Identity header card | ✓ VERIFIED | 94 lines; exports `EngagementProfileHeader`; photo + initials fallback wired |
| `components/mobile/EngagementProfileMetricGrid.tsx` | 2×2 metric grid | ✓ VERIFIED | 51 lines; exports `EngagementProfileMetricGrid`; contains `grid-cols-2` |
| `components/mobile/EngagementProfileBreakdown.tsx` | Activity breakdown card | ✓ VERIFIED | 96 lines; exports `EngagementProfileBreakdown`; three subsections present |
| `components/mobile/EngagementRecentEntries.tsx` | Collapsible time entries list | ✓ VERIFIED | 103 lines; exports `EngagementRecentEntries`; Collapsible from shadcn; `Number(entry.hours_worked).toFixed(1)` coercion |
| `components/mobile/EngagementRecentMeetings.tsx` | Collapsible meetings list | ✓ VERIFIED | 142 lines; exports `EngagementRecentMeetings`; Collapsible from shadcn; `Number(te.hours_worked).toFixed(1)` coercion |
### Key Link Verification
| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| `photo/route.ts` | `lib/services/msgraph-factory.ts` | `import { getMsgraphClient, isMsgraphConfigured }` | ✓ WIRED | Lines 3, 14, 39 — both functions imported and called |
| `photo/route.ts` | `lib/auth-utils.ts` | `import { requireAuth }` | ✓ WIRED | Line 2 imports, line 10 calls `requireAuth()` as first handler action |
| `photo/route.ts` | `MsGraphClient.getUserPhotoBytes` | method call | ✓ WIRED | Line 40 — `client.getUserPhotoBytes(userId)` |
| `page.tsx` | `/api/engagement/user/[userId]` | fetch in useEffect | ✓ WIRED | Line 93 — `fetch('/api/engagement/user/${userId}?period=${period}')` in useEffect with deps `[userId, period, retryNonce]` |
| `page.tsx` | `EngagementPeriodChips` | import + JSX | ✓ WIRED | Line 6 imports, lines 138 and 186 render `<EngagementPeriodChips>` |
| `EngagementProfileHeader.tsx` | `/api/mobile/engagement/user/[userId]/photo` | `<img src={...}/photo}` | ✓ WIRED | Line 58 — `src={'/api/mobile/engagement/user/${userId}/photo'}` |
| `EngagementProfileHeader.tsx` | `EngagementUserRow.tsx#getInitials` | named import | ✓ WIRED | Line 12 — `import { getInitials } from '@/components/mobile/EngagementUserRow'`; used at line 68 |
### Data-Flow Trace (Level 4)
| Artifact | Data Variable | Source | Produces Real Data | Status |
|----------|---------------|--------|--------------------|--------|
| `page.tsx` | `data` (ApiResponse) | `/api/engagement/user/[userId]` which queries `graph_users`, `engagement_snapshots`, `time_entries`, `teams_meetings` | Yes — route.ts lines 16-103 contain real SQL JOINs against Postgres tables | ✓ FLOWING |
| `EngagementProfileHeader.tsx` | `displayName`, `email`, `jobTitle`, `department`, `lastActiveAt` | Props from `page.tsx` from `data.user` | Yes — sourced from DB `graph_users` table | ✓ FLOWING |
| `EngagementProfileMetricGrid.tsx` | `hoursWorked`, `billableHours`, `daysWorked`, `meetingsAttended` | Props from `page.tsx` from `data.hours` | Yes — aggregated from `time_entries` + `engagement_snapshots` in the API | ✓ FLOWING |
| `EngagementRecentEntries.tsx` | `entries` | Props from `page.tsx` from `data.recentEntries` | Yes — real time_entries rows from Postgres; NUMERIC coerced via `Number(entry.hours_worked).toFixed(1)` | ✓ FLOWING |
| `EngagementRecentMeetings.tsx` | `meetings` | Props from `page.tsx` from `data.recentTeamsMeetings` | Yes — real teams_meetings rows with LEFT JOIN on attendees | ✓ FLOWING |
### Behavioral Spot-Checks
Step 7b: SKIPPED — photo proxy requires live MSGRAPH credentials and cannot be exercised without a running server and Graph access. Page rendering requires browser.
### Requirements Coverage
| Requirement | Source Plan | Description | Status | Evidence |
|-------------|-------------|-------------|--------|----------|
| ENG-06 | 08-01, 08-02 | User profile at `/mobile/engagement/[userId]`, single-column layout: identity header → period selector → key metrics → activity breakdown → recent items | ✓ SATISFIED | Real route at `app/mobile/engagement/[userId]/page.tsx`; render order confirmed; photo proxy powers avatar |
| ENG-07 | 08-02 | User profile is a real page, not a modal — back gesture works | PARTIAL (human_needed) | Real Next.js App Router route confirmed (not a modal). Scroll position restore attempted via sessionStorage shim but only partially works — user accepted. The "real page, not a modal" portion is verified; the "back gesture at same scroll position" requires human testing. |
| ENG-08 | 08-02 | Profile reuses existing engagement profile data endpoints; no new data | ✓ SATISFIED | `page.tsx:93` fetches `/api/engagement/user/${userId}?period=${period}` (existing endpoint); git log confirms the endpoint file was not modified by Phase 8 |
### Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
|------|------|---------|----------|--------|
| (none) | — | — | — | No TODOs, FIXMEs, placeholders, or empty implementations found in any Phase 8 file |
### Human Verification Required
#### 1. Scroll Restoration on Browser Back Gesture (SC#2)
**Test:** Open `/mobile/engagement` on a mobile device or narrow browser viewport. Scroll the employee list down past 3-4 rows. Tap one row to navigate to the profile. Then press the browser back button (or swipe-back on iOS/Android).
**Expected:** The overview page scrolls back to the same position — the same row that was tapped is visible without manual re-scrolling.
**Why human:** The sessionStorage shim (`app/mobile/engagement/page.tsx:175-225`) saves `window.scrollY` and `main.scrollTop` on navigation, then attempts restore via `requestAnimationFrame` retry on mount. Whether this reliably restores scroll depends on browser, Next.js App Router behavior, and the shell's `overflow-y-auto` vs `window.scroll` interaction. Per the SUMMARY and context notes, the implementation was attempted but user testing confirmed it does not consistently restore scroll. The user has accepted this as a known limitation. Verify the current behavior matches the acceptance (partial — page scrolls back to top, not prior position) and confirm no regression was introduced.
### Gaps Summary
No blocking gaps. All code artifacts exist, are substantive, and are wired to real data sources. The only open item (SC#2 scroll restoration) was user-tested during Phase 8 execution and accepted as a known limitation. It does not prevent the core goal ("manager taps a row, arrives at a real profile page") from being met — it is a UX polish concern on the return journey.
The `human_needed` status reflects that SC#2 needs to be acknowledged by the reviewer rather than re-verified mechanically.
---
_Verified: 2026-05-08_
_Verifier: Claude (gsd-verifier)_