6 KiB
| phase | plan | subsystem | tags | dependency_graph | tech_stack | key_files | decisions | metrics | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 07-engagement-overview-new | 01 | mobile-api |
|
|
|
|
|
|
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:
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: falsewith 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_usersfor Autotask hours
Endpoint: GET /api/mobile/engagement/trend
File: app/api/mobile/engagement/trend/route.ts
Returns EngagementTrendResponse:
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_seriesensures 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. Theintervalvalue 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/userslacksrequireAuth()— 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
app/api/mobile/engagement/summary/route.tsexistsapp/api/mobile/engagement/trend/route.tsexists- Commits
f4a9fd8andc3d370cexist in history npx tsc --noEmit --prettyexits 0- Both files export documented interfaces
requireAuth()called before anypostgresClient.queryin both files- No Zod imports in either file
- Period whitelist + 400 path in both files