wulf-pulse/.planning/phases/07-engagement-overview-new/07-CONTEXT.md

22 KiB
Raw Blame History

Phase 7: Engagement Overview (NEW) - Context

Gathered: 2026-05-04 (auto mode) Status: Ready for planning

## Phase Boundary

Build a phone-first refactor of the Engagement overview at /mobile/engagement — accessed exclusively from the More drawer (NOT the bottom bar). The page gives a manager a quick read on team engagement: a sticky 3-chip period selector under the H1, four stacked summary cards (active users / total Graph hours / total Autotask hours / hours-per-active-user), one compact "hours trend" sparkline at the top of the per-employee list, and a sortable + searchable list of stacked employee rows (avatar/initials, name, role, hours bar) sourced from the existing engagement data layer.

In scope:

  • New page app/mobile/engagement/page.tsx
  • 2 new mobile endpoints: /api/mobile/engagement/summary (4 totals for ENG-03) and /api/mobile/engagement/trend (daily hours time-series for ENG-05 sparkline)
  • Reuse of existing /api/engagement/users for the per-employee list (ENG-04)
  • New components: EngagementPeriodChips, EngagementSummaryCard, EngagementHoursSparkline, EngagementSortChips, EngagementSearchInput, EngagementUserRow, EngagementUserRowSkeleton
  • Confirm More drawer entry to /mobile/engagement is correctly wired (Phase 2 already added it per DRAWER-03; verify and don't regress)

Out of scope:

  • The user profile page at /mobile/engagement/[userId] — that's Phase 8 (ENG-06..08)
  • Multi-series charts on mobile (explicit spec out-of-scope §6.5, §7)
  • Mobile editing (read-only by design — PROJECT.md Out of Scope, REQ EDIT-01)
  • "Today" period chip — engagement_snapshots only aggregate at D7/D30/D90 granularity; a "today" period would require a new D1 sync and is out of spec scope for this phase
  • Modifying desktop /engagement/* pages or APIs (PROJECT.md Out of Scope: "Restyling or replacing the desktop pages…")
  • Sort by anything beyond the 3 ENG-04 axes (hours / name / utilization)
  • Server-side search (client-side filter satisfies ENG-04)
## Implementation Decisions

Page route, drawer entry, and shell integration

  • D-01: New page at app/mobile/engagement/page.tsx. The More drawer already routes here per DRAWER-03 (Phase 2). Verify the route works end- to-end after this phase lands; do not change MoreDrawer.tsx.
  • D-02: ENG-09 enforcement — Engagement is NOT on the bottom nav. The Phase 2 BottomNav.tsx is already correct (4 tabs + More cell, no Engagement). Do not modify BottomNav.tsx.
  • D-03: Page is 'use client' + useState + useEffect + fetch (CLAUDE.md: no SWR/react-query, no new state libs).

Period selector (ENG-02)

  • D-04: 3 chips: 7d, 30d, 90d. Mapped 1:1 to the data layer's period_type values D7, D30, D90 (per migrations/041_create_engagement_tables.sql). Default: 30d (D30) — matches the existing endpoints' default.
  • D-05: Sticky just below the page H1: sticky top-0 z-10 bg-background pt-2 pb-3 -mx-4 px-4 (offset for the page padding so the chips run edge-to-edge of the shell while content above scrolls).
  • D-06: Active chip: bg-primary text-primary-foreground. Inactive: bg-muted text-foreground hover:bg-muted/80. Chip shape: rounded-full px-3 py-1.5 text-xs font-semibold (matches mobile compact-control density). Three chips in a horizontal flex gap-2 row, no scroll.
  • D-07: Spec text says "today / 7d / 30d" but the data layer offers only D7/D30/D90 aggregates. We follow the data layer and document this as a deviation. "Today" is captured in <deferred> for a future D1 sync.

Summary cards (ENG-03)

  • D-08: 4 cards stacked single-column (no 4-up grid on phone widths). In order:
    1. Active users — count of users with engagement activity in the selected period (matches existing summary.activeThisPeriod)
    2. Total Graph hours — total Microsoft Graph activity hours (audio_duration_seconds + meeting_duration_seconds, summed across all active users in the period, converted to hours with 1 decimal)
    3. Total Autotask hours — total time_entries.hours_worked across all human resources matching graph_users in the period
    4. Hours per active usertotalAutotaskHours / activeUsers (one decimal). If activeUsers == 0, render "—"
  • D-09: New endpoint /api/mobile/engagement/summary returns these 4 metrics directly, accepting period=D7|D30|D90. Reason: existing /api/engagement/summary returns averages (avgHoursWorked, avgBillableHours, etc.), not the totals ENG-03 specifies. A thin mobile endpoint is cleaner than computing on the client.
  • D-10: Card visual: shadcn Card + CardContent. Big number (text-2xl font-semibold), small label below (text-xs text-muted-foreground). One card per row, space-y-3 between cards. Cards have no shadow, just border (matches FinanceRow density).

Hours trend sparkline (ENG-05)

  • D-11: Single compact sparkline at the top of the per-employee list, scoped to the selected period. Series: total Autotask hours per day for the period.
  • D-12: Custom inline SVG sparkline component — EngagementHoursSparkline — takes points: { date: string; hours: number }[] and renders a 3rem- tall path. Reason: DASH-04 precedent (no recharts on mobile); spec §6.5 ("no multi-series chart on mobile in this iteration"). One series, no axes, no tooltips. A faint baseline at 0 and a single colored line (stroke-primary stroke-2 fill-none).
  • D-13: Sparkline card has a small label row: Hours trend · last {period_label} left, latest-value (e.g. 12.4h today) right, both text-xs text-muted-foreground. Card height ~80px total.
  • D-14: New endpoint /api/mobile/engagement/trend returns { points: { date: string; hours: number }[] } for the period. Aggregates daily totals across time_entries joined to graph_users (same scope as the summary card filter). Period maps: D7 → 7 daily points, D30 → 30 daily points, D90 → 90 daily points (or 30 grouped weekly for D90 if perf matters — planner decides if 90 daily points renders cleanly at narrow viewport).
  • D-15: When points is empty, render the card with the period label and "No activity" inline — no broken empty SVG.

Per-employee list (ENG-04)

  • D-16: Reuse existing /api/engagement/users directly, no mobile wrapper. Existing response shape ({ users[], pagination }) is suitable.
  • D-17: Initial fetch: ?period={D7|D30|D90}&sort=billable_hours&order=desc&page=1 (page size 50 is the existing endpoint's fixed value).
  • D-18: Pagination strategy: page-based, infinite scroll via IntersectionObserver (mirror Phase 4/6 pattern). Sentinel triggers ?page=N+1 fetch when last row enters viewport. "Load more" fallback button below the sentinel, hidden when pagination.totalPages reached. Most teams have ≤50 staff so most users will only see one page.
  • D-19: Row shape (stacked card per user):
    • Top line: Avatar circle (initials from displayName, h-8 w-8) + displayName (text-sm font-semibold, 1-line truncate) + role (jobTitle if present, text-xs text-muted-foreground, 1-line truncate) — left side; total billable hours on right (text-sm font-semibold, e.g. "12.4h")
    • Hours bar: <div className="h-1.5 bg-muted rounded"> with inner <div> width = min(100%, billableHours / maxRowHours * 100%), bg-primary. maxRowHours = the largest billable hours value in the current page (computed client-side after fetch).
    • Tap target: wraps in <Link href={\/mobile/engagement/${graphUserId}`}>` so Phase 8 (already-planned) can pick up navigation. Even though Phase 8 builds the destination page, the link is wired here so Phase 7's row component is feature-complete; Phase 8 owns the page that receives the tap.

Sort + search (ENG-04)

  • D-20: Sort control above the list: 3 chips — Hours, Name, Utilization. Active chip: bg-primary text-primary-foreground. Tapping a chip triggers a refetch with the corresponding sort param. Default: Hours desc.
    • Hourssort=billable_hours&order=desc
    • Namesort=display_name&order=asc
    • Utilizationsort=billable_hours&order=desc (utilization isn't a direct sort on the endpoint; we sort by billable_hours and visually compute billableHours / hoursWorked ratio in the row. If a planner finds this confusing, pivot to sort=hours_worked and compute utilization = billable / total × 100% in row content)
  • D-21: Search input above the list: a single <Input> placeholder "Search by name or email", debounced 300ms, filters loaded users client-side (no server search param). Filter applies AFTER fetch — so it's instant on the visible page set but won't auto-load more pages when filter narrows results. Acceptable for ≤50-user teams; document in <deferred> if scale grows.
  • D-22: When the search filter has no results on the loaded set, render an inline "No matches for '{query}'" line with a "Clear search" button. Don't hide the page entirely.

Loading / empty / error states

  • D-23: Initial load → 4 summary card skeletons + 1 sparkline-card skeleton + 5 user-row skeletons. Reuse Skeleton from components/ui/skeleton.tsx.
  • D-24: Subsequent infinite-scroll → small inline spinner above the Load more button. Mirror Phase 4 D-21 / Phase 6 D-29.
  • D-25: Fetch errors → toast.error() (sonner) per failure; Load more button flips to "Retry". Mirror Phase 6 D-30.
  • D-26: Empty: when summary.activeUsers == 0 AND users.length == 0 for the selected period, render an EmptyState-style card: heading "No engagement data for this period", body "Try a different period or trigger a sync from /admin", with a Settings icon. Period chips remain interactive so the user can switch.
  • D-27: When configured: false from the summary endpoint (Microsoft Graph not configured), render a banner card: heading "Engagement sync not configured", body "Set MSGRAPH_* env vars and restart" with a link out to admin. Inherit existing isMsgraphConfigured() semantics from the existing endpoint.

Typography & spacing (mirror Phase 4 UI-SPEC)

  • D-28: Two weights only: font-normal (400) and font-semibold (600). No font-medium.
  • D-29: Four declared sizes (Phase 4 typography rule cap): text-sm (14px) primary, text-xs (12px) secondary/labels, text-[10px] for badges/captions, and text-2xl for the four big summary numbers. Page H1 "Engagement" uses text-sm font-semibold (matches Row primary scale; H1 scrolls away under the sticky period chips). text-base (16px) is NOT used on this page — kept the count at 4 to satisfy the UI-checker typography cap.
  • D-30: Page container: px-4 py-4 space-y-4 (matches Phase 5/6). No horizontal overflow at 360px viewport.

Page H1 placement (NAV-01 / spec §5.1)

  • D-31: <h1>Engagement</h1> renders in the page body, not the shell header (Phase 2 spec: "no page title in the header"). H1 sits above the period selector. The period selector is sticky relative to the page; the H1 scrolls away.

Auth + scoping

  • D-32: Both new endpoints use requireAuth() from lib/auth-utils.ts. No company scoping (kiosk_settings) — engagement data is org-wide and the desktop endpoints already operate org-wide (no per-company filter on /api/engagement/*). Mobile follows the same posture.
  • D-33: Existing /api/engagement/users does NOT use requireAuth() — it's an existing-product gap (similar to the IDOR posture noted in Phase 6). Document as inherited risk in the threat model; do NOT fix the desktop endpoint in this phase (the spec out-of-scope explicitly forbids modifying desktop pages/endpoints).

What NOT to change

  • D-34: Existing /api/engagement/* endpoints are unchanged.
  • D-35: Existing app/engagement/* desktop pages are unchanged.
  • D-36: No edits to lib/services/msgraph-* or lib/services/engagement-sync-service.ts — all read-only consumption.
  • D-37: No new state libraries; no SWR/react-query (CLAUDE.md).
  • D-38: No Zod in API routes (CLAUDE.md: "no Zod in API routes unless required").

Claude's Discretion

  • Exact sparkline math (linear interpolation across days, gap handling for missing days)
  • Whether to use BarChart rectangles or a path for the sparkline (recommend path for compactness)
  • Avatar fallback initials algorithm (recommend first letter of first + last word of displayName)
  • Whether to expand or hide the search input by default (recommend always-visible, unobtrusive)
  • Skeleton visual pattern density
  • Exact chip vs button styling for the period selector and sort toggle (recommend matching shadcn Toggle density)
  • Whether EngagementUserRow extracts a separate component (yes, for Phase 8 reuse — the user profile page may share the avatar + name identity block)

<canonical_refs>

Canonical References

Downstream agents MUST read these before planning or implementing.

Phase spec

  • docs/superpowers/specs/2026-05-03-mobile-shell-design.md §6.5 (Engagement Overview) — primary scope. §3.2 (More drawer) confirms Engagement entry. §7 lists explicit non-goals for this phase.
  • .planning/REQUIREMENTS.md (ENG-01..05, ENG-09) — locked acceptance criteria.

Project conventions

  • CLAUDE.md — Pulse stack rules (no SWR/react-query, no ORM, no Zod in API routes, port 3100), /mobile/* boundary, kebab-case files, PascalCase exports.
  • DESIGN.md — design tokens, component vocabulary, navigation IA.
  • ARCHITECTURE.md — engagement sync overview (read for context; sync pipeline itself is unchanged this phase).

Prior phase contracts (patterns to mirror)

  • .planning/phases/02-mobile-shell-more-drawer/02-CONTEXT.md — shell decisions; the engagement page docks under this layout. DRAWER-03 wires Engagement entry from the More drawer.
  • .planning/phases/03-dashboard-restyle/03-01-SUMMARY.mdKpiCardMobile pattern (the four summary cards mirror this scale).
  • .planning/phases/04-tickets-restyle/04-CONTEXT.md — IntersectionObserver
    • Load more pattern (D-08..D-14), URL-synced filter pattern (NOT used here — sort/search stays in component state per the lower data scale).
  • .planning/phases/04-tickets-restyle/04-UI-SPEC.md — typography contract (2 weights × 3 sizes), spacing scale, color tokens to mirror.
  • .planning/phases/05-finance-restyle/05-CONTEXT.md — Card/typography reuse pattern; inline error/Retry pattern (D-18); empty state convention (D-19).
  • .planning/phases/06-analyzer-feed-new/06-CONTEXT.md — Pattern for reusing existing data endpoints (D-25), pattern for adding a /api/mobile/* mobile endpoint when the existing shape doesn't fit (D-09 here mirrors 06-09 there).

Existing code (entry points)

  • app/api/engagement/summary/route.ts — desktop summary endpoint; reference for averages / period mapping / staff filter logic.
  • app/api/engagement/users/route.tsREUSED as-is by the mobile list. Pattern reference for the period mapping (D7|D30|D90), sort whitelist, and pagination shape.
  • app/api/engagement/user/[userId]/route.ts — Phase 8 scope, mentioned here only because Phase 7's row links to /mobile/engagement/[userId] which Phase 8 owns.
  • app/api/engagement/user/[userId]/history/route.ts — Phase 8 scope.
  • app/engagement/page.tsx — desktop overview (~1300 lines). Reference ONLY — DO NOT modify, DO NOT port; build mobile views from same data sources, phone-first.
  • app/engagement/profile/page.tsx — desktop profile (~650 lines). Reference ONLY — Phase 8 scope.
  • app/mobile/layout.tsx (Phase 2) — shell where the engagement page docks; no changes needed.
  • components/mobile/MoreDrawer.tsx (Phase 2) — already routes to /mobile/engagement per DRAWER-03; verify, don't modify.

Existing schema

  • migrations/041_create_engagement_tables.sqlgraph_users + engagement_snapshots tables. period_type enum: D7, D30, D90. engagement_snapshots.UNIQUE(user_email, period_type, period_end).
  • migrations/042_add_engagement_calendar_columns.sql — additional columns added later; read for additional fields available on snapshots if helpful.
  • The time_entries, resources, zoom_calls, zoom_meetings tables are joined for hours/zoom totals in the existing endpoints — same joins apply for the trend endpoint.

Components and primitives

  • components/ui/{card,badge,skeleton,empty-state,input,separator}.tsx — shadcn primitives in use across mobile phases.
  • components/mobile/KpiCardMobile.tsx — Phase 3 KPI card pattern; consider for the four summary cards (or its scale, if a separate component fits better).
  • components/mobile/AnalyzerFeedRow.tsx, components/mobile/FinanceRow.tsx — recent extracted-row component patterns to mirror for EngagementUserRow.
  • components/mobile/AnalyzerRowSkeleton.tsx — recent skeleton pattern.

</canonical_refs>

<code_context>

Existing Code Insights

Reusable Assets

  • requireAuth() from lib/auth-utils.ts — auth gate for new endpoints.
  • postgresClient.query() from lib/services/postgres-client.ts — parameterized SQL.
  • isMsgraphConfigured() from lib/services/msgraph-factory.ts — returns boolean; surface as banner per D-27.
  • engagement_snapshots + graph_users + time_entries + resources tables — already populated by the engagement sync (engagement-daily schedule, 6am).
  • shadcn primitives: Card, Badge, Skeleton, EmptyState, Input, Button, Separator. All in components/ui/.
  • lucide-react icons already in deps (Settings, Search, Loader2, Users, etc.).
  • IntersectionObserver — browser-native, no dep.
  • relTime() helper inline in app/mobile/tickets/page.tsx:29-37 — not relevant here (no time-ago) but the helper file shows the pattern.

Established Patterns

  • Mobile pages: 'use client' + useState + useEffect + fetch('/api/...')
  • API routes: NextResponse.json + requireAuth() from lib/auth-utils.ts
  • Postgres via postgresClient.query() parameterized SQL; manual snake_case → camelCase transform.
  • TypeScript interfaces exported from API route file alongside the handler; pages consume via import type { ... } from '@/app/api/.../route'.
  • IntersectionObserver pattern from app/mobile/tickets/page.tsx and app/mobile/analyzer/page.tsx (Phase 4 / Phase 6).
  • Skeleton render pattern from components/mobile/AnalyzerRowSkeleton.tsx / components/mobile/TicketRowSkeleton.tsx.

Integration Points

  • app/mobile/layout.tsx (Phase 2) renders the shell — /mobile/engagement docks inside it. Bottom nav is unaffected (Engagement is NOT a tab per ENG-09).
  • MoreDrawer.tsx (Phase 2) already routes to /mobile/engagement per DRAWER-03 — verify with a manual tap during human UAT.
  • The new /api/mobile/engagement/* endpoints sit under existing Better Auth + middleware (already requires auth for /api/* routes not in the public list per middleware.ts).
  • Phase 8 will build /mobile/engagement/[userId] — Phase 7's user row already wires <Link href={\/mobile/engagement/${graphUserId}`}>` so the navigation works as soon as Phase 8 lands.

</code_context>

## Specific Ideas
  • Mirror Phase 6's pattern of "reuse existing data endpoint where possible, add a thin /api/mobile/* endpoint only where the existing shape doesn't fit." Two new endpoints here is the minimum: one for totals (existing endpoint returns averages), one for time-series (no existing endpoint).
  • Sparkline should feel calm, not flashy: a single thin line, no dots, no axis labels, no animation, ~3rem tall. Linear's "monthly active" sparklines are a good reference.
  • Avatar initials: same algorithm as app/engagement/page.tsx if it has one; otherwise first letter of first word + first letter of last word of displayName. Stick to upper-case, neutral background.
  • Hours bar: keep it 6px tall (h-1.5); width should make it obvious who's putting in the most time without being a chart on its own. Don't add a numeric label inside the bar — the right-aligned hours number above the bar already shows the value.
## Deferred Ideas
  • "Today" period chip — engagement_snapshots only aggregate at D7/D30/D90. A D1 sync would require adding a new period_type and updating engagement-sync-service.ts. Defer to a future phase.
  • Server-side search on the per-employee list — client-side filter is fine at ≤50 staff. Add server-side search if the team grows past ~150 staff and the page=1 fetch can no longer cover all visible results.
  • Multi-series trend chart (Graph hours vs Autotask hours overlaid) — explicitly out-of-scope per spec §6.5 ("no multi-series chart on mobile"). Desktop already has this.
  • Per-row drill-down to the user profile — that's Phase 8 (ENG-06..08). Phase 7 wires the <Link> only.
  • Sort by zoom calls / meetings / emails — outside ENG-04's three sort axes. Add if managers request.
  • "Engagement sync now" button on mobile — admin action, lives on the desktop /admin page. Mobile is read-only (REQ EDIT-01).
  • IDOR fix on existing /api/engagement/* endpoints — inherited risk from the existing product. Out of scope per spec §7 ("Restyling or replacing the desktop pages reachable from the More drawer — desktop pages stay as they are"). Track in STATE.md follow-up; recommend a future security phase.
  • Response virtualization for the per-employee list — page size is 50 and most teams have ≤50 staff, so a single rendered list is fine for v1. Add react-window or similar only if perf measurement warrants.

Phase: 07-engagement-overview-new Context gathered: 2026-05-04