feat(insights): batch the derived-metric reads behind one route

The Insights cold mount fired 14+ independent GET /api/insights/derived
requests — Readiness, Sleep and the vitals baselines are compute-on-visit,
and all of them share one Prisma pool. That is the pool-starvation class
that surfaces as the app hanging then recovering.

Add GET /api/insights/derived/batch?metrics=a,b,c[:type] that resolves the
requested metrics server-side under a bounded p-limit(4) with the profile
loaded once via the shared loadBaselineProfile, returning a map of the same
flat Derived<T> envelope keyed by the request token. Add the useDerivedBatch
hook + the insightsDerivedBatch query-key factory entry; the single-metric
route + hook stay for the per-score detail pages. The single route now also
adopts loadBaselineProfile instead of re-inlining the profile read. Honors
the existing 8s ceiling, retry:0, replace-with-fallback, and warms nothing
on visit. Register the new route in the OpenAPI contract.
This commit is contained in:
Marc-André Bombeck committed 2026-06-03 00:04:56 +02:00
1 parent 3f8c8e750c
commit b92d5cec82
8 files changed
+561 -16

No files matched your search

+55
View File
@@ -1406,6 +1406,36 @@ const derivedMetricResponse = z
"Flat `Derived<T>` envelope for one derived wellness metric. Pure compute over the rollup tier (no LLM, no narrative). iOS decodes one stable shape and combines values across metrics; coverage/confidence/provenance let it render the same honesty chips.",
});
// v1.10.0 — batched derived-metric query. The `metrics` CSV carries one
// or more `metric` / `metric:type` tokens; the route fans out server-side
// under a bounded limiter with the profile loaded once, collapsing the
// dashboard's cold-mount fan-out of N single-metric requests into one.
const derivedBatchQuery = z
.object({
metrics: z
.string()
.min(1)
.max(1024)
.describe(
"Comma-separated derived-metric tokens. Each is a `<DERIVED_METRIC_ID>` or `<DERIVED_METRIC_ID>:<MeasurementType>` (the colon sub-targets a VITALS_BASELINE vital). An unknown id 422s; at most 24 tokens; duplicates collapse.",
),
})
.meta({ id: "DerivedBatchQuery" });
const derivedBatchResponse = z
.object({
metrics: z
.record(z.string(), derivedMetricResponse)
.describe(
"Map keyed by the per-request token (`<metric>` or `<metric>:<type>`). Each value is the same flat `Derived<T>` envelope the single-metric route returns, so a client decodes one shape and reads back exactly the tokens it asked for.",
),
})
.meta({
id: "DerivedBatchResponse",
description:
"Batched derived-metric values. One request resolves the whole dashboard grid (the wellness scores + the derived re-frames + one baseline per vital) instead of N concurrent single-metric requests sharing the Prisma pool. Pure compute over the rollup tier — no LLM, no narrative, no cache table.",
});
// v1.10.0 — FDR-controlled correlation discovery result. One discovered,
// statistically-defensible behaviour → next-day-outcome pair.
const discoveredCorrelation = z
@@ -3190,6 +3220,31 @@ export const openApiPaths: NonNullable<ZodOpenApiObject["paths"]> = {
},
},
},
"/api/insights/derived/batch": {
get: {
tags: ["Insights"],
summary: "Derived wellness metrics (batched compute-once)",
description:
"v1.10.0 — resolve several derived wellness metrics in ONE request. The `metrics` CSV names the metrics (a `metric:type` token sub-targets a VITALS_BASELINE vital); the server fans out under a bounded limiter with the profile loaded once and returns a map keyed by the per-request token. Collapses the Insights cold-mount fan-out of 14+ independent single-metric requests — the pool-starvation class that surfaces as a hang-then-recover. The single-metric route stays for the per-score detail pages. Auth via cookie or Bearer.",
requestParams: {
query: derivedBatchQuery,
},
responses: {
"200": {
description: "The map of derived-metric values, keyed by token.",
content: {
"application/json": {
schema: dataEnvelope(
derivedBatchResponse,
"DerivedBatchResponseEnvelope",
),
},
},
},
...stdResponses,
},
},
},
"/api/insights/correlations": {
get: {
tags: ["Insights"],