From ecc7177832ad76e2747ef5df6064d4e3602a6eca Mon Sep 17 00:00:00 2001 From: lorentz Date: Sun, 3 May 2026 22:45:15 -0400 Subject: [PATCH] docs(07-01): complete mobile engagement API endpoints plan summary --- .../07-01-SUMMARY.md | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 .planning/phases/07-engagement-overview-new/07-01-SUMMARY.md diff --git a/.planning/phases/07-engagement-overview-new/07-01-SUMMARY.md b/.planning/phases/07-engagement-overview-new/07-01-SUMMARY.md new file mode 100644 index 0000000..10bfa73 --- /dev/null +++ b/.planning/phases/07-engagement-overview-new/07-01-SUMMARY.md @@ -0,0 +1,131 @@ +--- +phase: 07-engagement-overview-new +plan: 01 +subsystem: mobile-api +tags: [mobile, engagement, api, auth, typescript] +dependency_graph: + requires: + - migrations/041_create_engagement_tables.sql + - migrations/042_add_engagement_calendar_columns.sql + - lib/auth-utils.ts + - lib/services/postgres-client.ts + - lib/services/msgraph-factory.ts + provides: + - app/api/mobile/engagement/summary/route.ts + - app/api/mobile/engagement/trend/route.ts + affects: + - app/mobile/engagement/page.tsx (Plan 03 consumer) +tech_stack: + added: [] + patterns: + - requireAuth() gate before any DB query + - Period whitelist validation with 400 rejection + - Manual snake_case → camelCase transform + - Exported TypeScript interfaces from route files + - Parameterized SQL via postgresClient.query() +key_files: + created: + - app/api/mobile/engagement/summary/route.ts + - app/api/mobile/engagement/trend/route.ts + modified: [] +decisions: + - "Used named import { postgresClient } matching existing codebase pattern (not default import)" + - "interval interpolation in SQL is safe: value comes from static map keyed on whitelisted period, never user input" + - "trend endpoint uses generate_series to guarantee continuous daily series with zero-fill for missing days" + - "lastSynced field omitted from MobileEngagementSummary (not in interface spec; PLAN-spec is authoritative)" +metrics: + duration: "~10 min" + completed: "2026-05-04" + tasks: 2 + files_created: 2 + files_modified: 0 +--- + +# Phase 7 Plan 01: Mobile Engagement API Endpoints Summary + +Two new read-only mobile engagement endpoints with auth gates, period whitelist validation, and exported TypeScript interfaces for consumption by Plan 03. + +## What Was Built + +### Endpoint: `GET /api/mobile/engagement/summary` + +File: `app/api/mobile/engagement/summary/route.ts` + +Returns `MobileEngagementSummary`: + +```ts +export interface MobileEngagementSummary { + configured: boolean; // isMsgraphConfigured() + activeUsers: number; // distinct users with Teams/email activity in period + totalGraphHours: number; // sum(audio + meeting seconds) / 3600, 1 decimal + totalAutotaskHours: number; // sum time_entries.hours_worked for matched resources, 1 decimal + hoursPerActiveUser: number; // totalAutotaskHours / activeUsers (0 if activeUsers === 0) +} +``` + +- Default period when missing: `D30` +- Period whitelist: `['D7', 'D30', 'D90']` — anything else returns 400 +- When no snapshot data exists for the period: returns zeroed response (no 503) +- When MSGRAPH not configured: returns `configured: false` with zeroed totals +- Staff filter: `account_enabled = true`, `LOWER(email) LIKE '%@wulfconsulting.%'`, excludes `#ext#` accounts, excludes pure-outbound service accounts (`notAutomatedFilter`) +- SQL joins: `engagement_snapshots` → `graph_users` → `resources` (DISTINCT ON deduplication) for snapshot metrics; `time_entries` → `resources` → `graph_users` for Autotask hours + +### Endpoint: `GET /api/mobile/engagement/trend` + +File: `app/api/mobile/engagement/trend/route.ts` + +Returns `EngagementTrendResponse`: + +```ts +export interface SparklinePoint { + date: string; // "YYYY-MM-DD" + hours: number; // total Autotask hours for that day (0 if no entries) +} +export interface EngagementTrendResponse { + points: SparklinePoint[]; // D7→7, D30→30, D90→90 points in ascending order +} +``` + +- `generate_series` ensures every day in the window has a row — zero-fills days with no time entries (continuous series for sparkline, no gaps) +- Points ordered ascending (oldest → most recent) so sparkline SVG renders left-to-right with most recent on the right +- Bounded result: whitelist caps period to max 90 rows (T-07-03) +- Same email scope/filters as summary endpoint + +## Period Whitelist Values + +| Chip label | API `period` param | SQL interval | Points count | +|---|---|---|---| +| 7d | D7 | 7 days | 7 | +| 30d | D30 | 30 days | 30 | +| 90d | D90 | 90 days | 90 | + +## Security Notes (Threat Model) + +- **T-07-01 (AuthN):** `requireAuth()` is the first statement in both handlers — DB queries only execute after a valid session is confirmed. +- **T-07-02 (Injection):** `parsePeriod()` validates against the whitelist before any SQL runs. The `interval` value comes from a static map keyed on the whitelisted period string — interpolation is on a fixed constant, never raw user input. +- **T-07-03 (DoS):** `generate_series` + whitelist caps trend to max 90 rows; no user-supplied row-count param. +- **T-07-04 (Info Disclosure):** Both endpoints return only aggregate scalars — no per-user PII, no enumerated Teams/email metadata. +- **T-07-05 (Inherited risk):** Existing `/api/engagement/users` lacks `requireAuth()` — inherited gap per D-33. Out of scope per D-34 and PROJECT.md. Documented here for follow-up in a future security phase. + +## Deviations from Plan + +None — plan executed exactly as written. The `{ postgresClient }` named import was used to match the existing codebase pattern (both summary and tickets endpoints use the named import, though the module exports both named and default). + +## Confirmed Inherited Risk (D-33) + +The existing `/api/engagement/users` endpoint (reused as-is by Plan 03 for the per-employee list) does not call `requireAuth()`. This is an existing-product gap. The new mobile endpoints do NOT increase this exposure (middleware.ts provides a session cookie gate for `/api/*` routes not in the public list). Recommend a dedicated security phase to add `requireAuth()` to the desktop engagement endpoints. + +## Known Stubs + +None. Both endpoints are fully wired to the database — no hardcoded empty values or mock data. + +## Self-Check + +- [x] `app/api/mobile/engagement/summary/route.ts` exists +- [x] `app/api/mobile/engagement/trend/route.ts` exists +- [x] Commits f4a9fd8 and c3d370c exist in history +- [x] `npx tsc --noEmit --pretty` exits 0 +- [x] Both files export documented interfaces +- [x] `requireAuth()` called before any `postgresClient.query` in both files +- [x] No Zod imports in either file +- [x] Period whitelist + 400 path in both files