The AI Executive view
/executive is a cross-survey, AI-powered leadership summary. It is deterministic-first: every
number, anomaly, forecast and risk is computed from the existing reporting aggregates, and an AI
narrative is layered on top — so the page never fails when the model is down.
Where it lives
- Service —
api/src/domains/insights.ts→ExecutiveInsightsService(db, ai). - Routes —
api/src/routes/executive.ts:GET /v1/executive/insights(the payload) andPOST /v1/executive/ask(natural-language Q&A grounded in the same snapshot). Gated on theexecutive.viewpermission and theexecutivefeature flag. - Page —
frontend/src/pages/ExecutivePage.tsx.
How a report is built
- Gather one deterministic snapshot in a single tenant transaction, reusing existing reporting
methods (
npsSummaryAll,metricAll,volumeByDayAll,channelBreakdown,surveyPortfolio,leaderboard,alertStatusCounts/alertTotalsByRule,verbatim.sentimentBreakdown,keyDrivers) over a current + prior window (theReportFilterhas no prior-period field — two windows are computed). No new SQL was added. - Compute deterministic insights: KPI deltas, an NPS forecast (least-squares projection), anomalies, a risk radar and an alerts digest.
- Enrich with one constrained-JSON Gemini call (
ai.completeJson) → headline, summary, emerging themes, recommended actions, agent coaching, driver storyline, forecast framing. On failure the deterministic payload is returned withai.available = false.
Caching
The full payload is cached server-side for 12 hours per (client_account, period/scope) in
system.executive_insight_cache (owner connection, account-keyed; Database.get/setExecInsightCache).
Repeat views and multiple viewers reuse it instead of re-running ~25 aggregates + the Gemini call. The
Refresh button forces a recompute (?refresh=1); an AI-down result is not cached (so the next
view retries). cachedAt drives the "Updated …" indicator.
Filtering
The Executive filter bar shows only the time period, defaulting to the last 30 days; the report uses that window. Survey/channel scope still applies for a scoped member (it's part of the cache key).