docs(07-01): complete mobile engagement API endpoints plan summary

This commit is contained in:
lorentz 2026-05-03 22:45:15 -04:00
parent 1ec561bc29
commit ecc7177832

View file

@ -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