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

6 KiB

phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
07-engagement-overview-new 01 mobile-api
mobile
engagement
api
auth
typescript
requires provides affects
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
app/api/mobile/engagement/summary/route.ts
app/api/mobile/engagement/trend/route.ts
app/mobile/engagement/page.tsx (Plan 03 consumer)
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()
created modified
app/api/mobile/engagement/summary/route.ts
app/api/mobile/engagement/trend/route.ts
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)
duration completed tasks files_created files_modified
~10 min 2026-05-04 2 2 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:

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_snapshotsgraph_usersresources (DISTINCT ON deduplication) for snapshot metrics; time_entriesresourcesgraph_users for 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_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

  • app/api/mobile/engagement/summary/route.ts exists
  • app/api/mobile/engagement/trend/route.ts exists
  • Commits f4a9fd8 and c3d370c exist in history
  • npx tsc --noEmit --pretty exits 0
  • Both files export documented interfaces
  • requireAuth() called before any postgresClient.query in both files
  • No Zod imports in either file
  • Period whitelist + 400 path in both files