diff --git a/.planning/phases/07-engagement-overview-new/07-CONTEXT.md b/.planning/phases/07-engagement-overview-new/07-CONTEXT.md new file mode 100644 index 0000000..702f540 --- /dev/null +++ b/.planning/phases/07-engagement-overview-new/07-CONTEXT.md @@ -0,0 +1,427 @@ +# 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 `` 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 user** — `totalAutotaskHours / 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:** `
` with inner + `
` 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 `` + 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. + - `Hours` → `sort=billable_hours&order=desc` + - `Name` → `sort=display_name&order=asc` + - `Utilization` → `sort=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 `` 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 `` 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:** Three sizes: `text-sm` (14px) primary, `text-xs` (12px) + secondary/labels, `text-[10px]` for badges/captions. Plus `text-2xl` + for the four big summary numbers and `text-base font-semibold` for the + page H1 "Engagement". +- **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:** `

Engagement

` 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 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.md` — `KpiCardMobile` + 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.ts` — **REUSED 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.sql` — `graph_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. + + + + +## 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 `` + so the navigation works as soon as Phase 8 lands. + + + + +## 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 `` 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* diff --git a/.planning/phases/07-engagement-overview-new/07-DISCUSSION-LOG.md b/.planning/phases/07-engagement-overview-new/07-DISCUSSION-LOG.md new file mode 100644 index 0000000..84a4287 --- /dev/null +++ b/.planning/phases/07-engagement-overview-new/07-DISCUSSION-LOG.md @@ -0,0 +1,152 @@ +# Phase 7: Engagement Overview (NEW) - Discussion Log + +> **Audit trail only.** Do not use as input to planning, research, or execution agents. +> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered. + +**Date:** 2026-05-04 +**Phase:** 07-engagement-overview-new +**Mode:** auto (recommended defaults selected for every gray area) +**Areas discussed:** Period selector mapping, Summary metric semantics, Per- +employee list data source, Sparkline data, List pagination, Search, Sort +options, Sparkline implementation, Loading/error/empty states, Typography & +spacing + +--- + +## Period selector mapping + +| Option | Description | Selected | +|--------|-------------|----------| +| Spec-literal: today / 7d / 30d | Matches spec text but data layer has no D1 aggregate | | +| Data-aligned: 7d / 30d / 90d | Maps to existing D7/D30/D90 period_type values | ✓ | +| Custom (date range picker) | Heavier UX, not in ENG-02 | | + +**Auto-selection:** 7d / 30d / 90d — preserves data-layer fidelity. +"Today" deferred (would need new D1 sync). + +--- + +## Summary metric semantics + +| Option | Description | Selected | +|--------|-------------|----------| +| Reuse existing /api/engagement/summary (averages) and compute totals client-side | Simple, but math on client and avg×count is approximate | | +| New /api/mobile/engagement/summary returning the 4 ENG-03 metrics directly | Cleanest; mirrors Phase 6 pattern of one mobile endpoint | ✓ | +| Extend existing endpoint with totals fields | Couples desktop + mobile semantics | | + +**Auto-selection:** New mobile endpoint — clean separation, accurate totals. + +--- + +## Per-employee list data source + +| Option | Description | Selected | +|--------|-------------|----------| +| Reuse existing /api/engagement/users directly | Already returns the right shape with sort/page params | ✓ | +| Wrap in /api/mobile/engagement/users | Adds a thin mobile-only endpoint with no value-add | | +| Inline SQL in the mobile page | Anti-pattern; violates server/client separation | | + +**Auto-selection:** Reuse existing — already shapes the data correctly. + +--- + +## Sparkline data source + +| Option | Description | Selected | +|--------|-------------|----------| +| New /api/mobile/engagement/trend (daily totals) | Required — no existing endpoint returns time-series | ✓ | +| Reuse engagement_snapshots client-side | snapshots aren't daily; can't compute trend client-side | | +| Skip the sparkline | Violates ENG-05 | | + +**Auto-selection:** New trend endpoint — required for ENG-05. + +--- + +## List pagination + +| Option | Description | Selected | +|--------|-------------|----------| +| Page-based + IntersectionObserver | Existing endpoint is page-based; matches Phase 4/6 UX | ✓ | +| Cursor-based (rewrite endpoint) | Requires modifying desktop endpoint (out of scope) | | +| No pagination (load all) | Page size 50; fine for ≤50 staff but breaks at scale | | + +**Auto-selection:** Page-based + IntersectionObserver — same UX as Phase 4/6. + +--- + +## Search behavior + +| Option | Description | Selected | +|--------|-------------|----------| +| Client-side filter on loaded users | Instant feedback, no server param needed at ≤50 staff | ✓ | +| Server-side search param | Requires modifying desktop endpoint | | +| Skip search | Violates ENG-04 | | + +**Auto-selection:** Client-side filter — fine at current scale. + +--- + +## Sort axes + +| Option | Description | Selected | +|--------|-------------|----------| +| 3 chips: Hours / Name / Utilization | Matches ENG-04 exactly | ✓ | +| Dropdown with 5+ axes | Too many for phone | | +| No sort control | Violates ENG-04 | | + +**Auto-selection:** 3 chips matching ENG-04. + +--- + +## Sparkline implementation + +| Option | Description | Selected | +|--------|-------------|----------| +| Custom inline SVG path | No chart library, 30 lines, fully controlled | ✓ | +| recharts LineChart | Already a dep, but DASH-04 forbids recharts on mobile | | +| Visx/d3 | New dep, overkill for one sparkline | | + +**Auto-selection:** Custom SVG — DASH-04 precedent. + +--- + +## Loading / empty / error states + +| Option | Description | Selected | +|--------|-------------|----------| +| Skeleton + toast.error + Retry + EmptyState card | Phase 4/5/6 precedent | ✓ | +| Single spinner only | Less polished | | +| Server-rendered placeholder | Doesn't match the client-fetch pattern | | + +**Auto-selection:** Mirror established mobile patterns. + +--- + +## Typography & spacing + +| Option | Description | Selected | +|--------|-------------|----------| +| Mirror Phase 4 UI-SPEC: 2 weights / 3 sizes + base/2xl extras | Consistency across mobile shell | ✓ | +| New scale just for Engagement | Avoid divergence cost | | + +**Auto-selection:** Mirror Phase 4/5/6. + +--- + +## Auto-Resolved (`--auto` mode) + +All ten gray areas were auto-resolved with the recommended option per +the workflow's `--auto` mode. No interactive questioning occurred. + +## Deferred Ideas + +(See `07-CONTEXT.md` `` section for the canonical list.) + +- "Today" period chip (requires D1 sync) +- Server-side search at scale +- Multi-series trend chart (out of spec) +- Per-row drill-down to user profile (Phase 8 owns this) +- Sort by zoom calls / meetings / emails +- "Engagement sync now" button on mobile (read-only by design) +- IDOR fix on existing /api/engagement/* endpoints (desktop, out of scope) +- List virtualization (deferred until scale demands)