feat: baseline-drift, breathing-screening and lab-change insight cards

Add three read-only awareness cards to the Insights overview, each drawn
off an existing engine and each honest about empty state — the section
un-mounts rather than surfacing a husk.

- Health status: surfaces what is drifting from the user's own normal by
  folding the personal-band deviations (coincident-deviation engine) with
  the dated, sustained level shifts (changepoint detector) at
  GET /api/insights/health-status. Awareness only, never a diagnosis.
- Breathing screening: summarises the last 30 nights of the per-night
  sleep-breathing-disturbance index plus device-flagged events at
  GET /api/insights/breathing-screening. Mirrors the device's own
  classification and states plainly that it is a screening signal, not a
  diagnosis — discuss with a clinician.
- Lab changes: pairs the two most-recent numeric lab panels and reports
  each shared analyte's signed delta and reference-band standing at
  GET /api/insights/labs-changes. Neutral framing only.

Each reshaping helper is pure and unit-tested for its present/absent
states. Register the three sections in the insights-layout universe so
they merge in as default-visible, wire them into the overview and the
edit-mode title map, add the query-factory keys, document the routes in
the OpenAPI contract, and translate every new string across all six
locales.
This commit is contained in:
Marc-André Bombeck committed 2026-06-28 03:20:49 +02:00
1 parent 31c35baec2
commit ec3fcea84f
27 files changed
+2148 -13

No files matched your search

+4
View File
@@ -36,9 +36,11 @@ import { biomarkerPaths } from "./biomarkers";
import { devicePaths } from "./devices";
import { exportPaths } from "./export";
import { healthRecordPaths } from "./health-record";
import { hydrationPaths } from "./hydration";
import { illnessPaths } from "./illness";
import { importPaths } from "./import";
import { insightsPaths } from "./insights";
import { insightsSignalPaths } from "./insights-signals";
import { labsPaths } from "./labs";
import { ocrPaths } from "./ocr";
import { measurementPaths } from "./measurements";
@@ -72,6 +74,7 @@ export const openApiPaths: NonNullable<ZodOpenApiObject["paths"]> = {
...coachFeedbackPaths,
...adminDiagnosticPaths,
...insightsPaths,
...insightsSignalPaths,
...moodPaths,
...settingsPaths,
...consentPaths,
@@ -82,6 +85,7 @@ export const openApiPaths: NonNullable<ZodOpenApiObject["paths"]> = {
...biomarkerPaths,
...illnessPaths,
...onboardingPaths,
...hydrationPaths,
};
export const openApiComponents: NonNullable<ZodOpenApiObject["components"]> = {
+177
View File
@@ -0,0 +1,177 @@
/**
* OpenAPI route module — v1.25 read-only insight signals.
*
* Three calm awareness reads built on existing engines:
* - GET /api/insights/health-status baseline-drift (bands + changepoints)
* - GET /api/insights/breathing-screening sleep-breathing screening signal
* - GET /api/insights/labs-changes "what changed since your last panel"
*
* Each is pure compute over the rollup / lab tier — no provider call. Labels
* are resolved client-side from the type tokens, so the wire carries tokens
* only. Part of the OpenAPI route table; aggregated in `./index.ts`.
*/
import { z } from "zod/v4";
import type { ZodOpenApiObject } from "zod-openapi";
import { dataEnvelope, stdResponses } from "./shared";
const healthStatusResponse = z
.object({
present: z
.boolean()
.describe("True when at least one deviation or shift is surfaced."),
deviations: z.array(
z.object({
type: z.string().describe("MeasurementType token."),
value: z.number().describe("Today's value."),
center: z.number().describe("Personal band center (median)."),
low: z.number(),
high: z.number(),
direction: z.enum(["above", "below"]),
}),
),
shifts: z.array(
z.object({
metric: z.string().describe("MeasurementType token."),
breakDate: z
.string()
.describe("YYYY-MM-DD of the new level's first day."),
beforeMean: z.number(),
afterMean: z.number(),
direction: z.enum(["up", "down"]),
}),
),
generatedAt: z.string().describe("ISO-8601 instant the read was computed."),
})
.meta({
id: "InsightsHealthStatus",
description:
"Vitals drifting from the user's personal normal — band deviations plus dated, sustained level shifts. Awareness only, never a diagnosis.",
});
const breathingScreeningResponse = z
.object({
present: z.boolean(),
nights: z.number().int().describe("Nights with a per-night index reading."),
recentMeanIndex: z
.number()
.nullable()
.describe("Mean of the index readings (lower-better); null when none."),
trend: z
.enum(["up", "down", "stable"])
.nullable()
.describe("Recent index vs the prior window; null when too few nights."),
eventCount: z
.number()
.int()
.describe("Device-flagged breathing-disturbance / apnea events."),
classification: z
.enum(["not-elevated", "elevated"])
.nullable()
.describe("The device's own classification; null when no data."),
generatedAt: z.string(),
})
.meta({
id: "InsightsBreathingScreening",
description:
"Sleep-breathing-disturbance screening signal — a screening signal only, never a diagnosis.",
});
const labsChangesResponse = z
.object({
present: z.boolean(),
latestDate: z
.string()
.nullable()
.describe("YYYY-MM-DD of the most-recent panel."),
previousDate: z
.string()
.nullable()
.describe("YYYY-MM-DD of the prior panel."),
changes: z.array(
z.object({
analyte: z.string(),
unit: z.string(),
latest: z.number(),
previous: z.number(),
delta: z.number().describe("Signed latest − previous."),
direction: z.enum(["up", "down", "flat"]),
status: z.enum(["in-range", "below", "above", "unknown"]),
}),
),
generatedAt: z.string(),
})
.meta({
id: "InsightsLabsChanges",
description:
"Per-analyte change between the two most-recent numeric lab panels. Neutral framing, never a diagnosis.",
});
export const insightsSignalPaths: NonNullable<ZodOpenApiObject["paths"]> = {
"/api/insights/health-status": {
get: {
tags: ["Insights"],
summary: "Baseline-drift health status",
description:
"Surfaces vitals drifting from the user's personal normal — out-of-band deviations from the personal-baseline engine plus dated, sustained level shifts from the changepoint detector. Pure compute over the rollup tier; awareness only, never a diagnosis. Requires the insights module. Cookie or Bearer auth.",
responses: {
"200": {
description: "The drift summary.",
content: {
"application/json": {
schema: dataEnvelope(
healthStatusResponse,
"InsightsHealthStatusEnvelope",
),
},
},
},
...stdResponses,
},
},
},
"/api/insights/breathing-screening": {
get: {
tags: ["Insights"],
summary: "Sleep-breathing screening signal",
description:
"Summarises the last ~30 nights of the per-night sleep-breathing-disturbance index plus device-flagged events into a calm awareness read. A screening signal only — never a HealthLog diagnosis. Requires the insights module. Cookie or Bearer auth.",
responses: {
"200": {
description: "The breathing-screening summary.",
content: {
"application/json": {
schema: dataEnvelope(
breathingScreeningResponse,
"InsightsBreathingScreeningEnvelope",
),
},
},
},
...stdResponses,
},
},
},
"/api/insights/labs-changes": {
get: {
tags: ["Insights"],
summary: "What changed since your last lab panel",
description:
"Pairs the two most-recent numeric lab panels and reports each shared analyte's signed delta plus its reference-band standing. Absent when there are fewer than two panels or no analyte is shared. Neutral framing, never a diagnosis. Cookie or Bearer auth.",
responses: {
"200": {
description: "The lab-change summary.",
content: {
"application/json": {
schema: dataEnvelope(
labsChangesResponse,
"InsightsLabsChangesEnvelope",
),
},
},
},
...stdResponses,
},
},
},
};