fix(insights): cap status-card provider calls at 20s with graceful fallback

The seven `*-status.ts` routes awaited `provider.generateCompletion`
with no client timeout. When the upstream stalled, React-Query's
three default retries plus exponential backoff stacked on top of
the upstream latency and the InsightStatusCard spun for up to 90 s
before the user saw the cached fallback or a generic failure. This
is the downstream half of FB-D2 ("pulse insights loads forever").

Add a small `withTimeout` helper that races the provider call
against a 20 s budget and resolves with a typed `{ timedOut, value }`
envelope. Every status route returns the no-key fallback text in a
cached-style response shape on timeout; the audit-log cache is NOT
written so a transient upstream stall cannot poison tomorrow's
hit. `retry: 0` on `useInsightStatus` stops the client from
re-firing the same expensive call after the deterministic fallback
lands.

Cap the per-route Prisma reads at the same time — `take: 365` for
PULSE / WEIGHT, `take: 1095` for the BP three-channel surfaces,
`take: 90` for MoodEntry, `take: 5000` for the general snapshot.
The downstream `applyPayloadBudget` trims further; without the cap
power users pulled tens of thousands of rows on every refresh.

iOS contract — `/api/insights/*-status` routes are web-only (iOS
runs its own on-device summaries against `/api/measurements/series`).
No public-API shape changes; pure server-internal hardening.
This commit is contained in:
Marc-André Bombeck committed 2026-05-16 00:28:31 +02:00
1 parent b00be2863d
commit 0d591ac928
10 files changed
+540 -174

No files matched your search

+5
View File
@@ -78,5 +78,10 @@ export function useInsightStatus(metric: InsightStatusMetric) {
},
enabled: isAuthenticated,
staleTime: 60 * 1000,
// v1.4.28 FB-D2 — the status route now returns a deterministic
// cached-style envelope on provider timeout. Retrying that response
// re-fires the same expensive upstream call and the user perceives
// a longer hang. Drop retries to surface the fallback immediately.
retry: 0,
});
}