mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
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:
1 parent
31c35baec2
commit
ec3fcea84f
27 files changed
+2148
-13
No files matched your search
@@ -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"]> = {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user