mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 14:40:52 +00:00
feat(insights): generic per-metric assessment coverage for HealthKit metrics
Extend the assessment-card system beyond the seven specialised scopes to the ~30 HealthKit metric pages, without hand-writing a prompt per metric. - Add a metric-metadata registry keyed by HealthKit id (display name, unit, direction, optional normal range, archetype, scope), covering the data-bearing HealthKit metrics. The seven specialised metrics keep their own scopes and are excluded from the generic set; an unregistered id gets no card. - Add a generic data-driven generator that reuses the existing graded snapshot, provider runner, stale-while-revalidate cache, and persist path. One prompt template per archetype (physiological-vital, activity-fitness, body-composition, mobility-gait, environmental-exposure) plus a dedicated sleep template, parameterised by metric metadata. Empty-data metrics return an insufficient marker with no provider call. Normal ranges use age + sex where the profile stores them; no ancestry/region ranges. - Add GET /api/insights/metric-status?metric=<id>: apiHandler-wrapped, Zod-validated against the closed registry id set (422 on unknown), read-only with stale-while-revalidate. First insights route with OpenAPI coverage; regenerate docs/api/openapi.yaml. - Route the generic metric:<id> scope through the on-demand status queue worker dispatch. - Extend the nightly warm pass to the generic metric caches for a user's data-bearing metrics only (one distinct read gates the set), keeping de+en warm and never calling a provider for an empty metric. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit 41453fbdfee60793c16269b9a9e5f04b6c3ea450)
This commit is contained in:
1 parent
2717297dae
commit
e33911e2d4
14 files changed
+1799
-4
No files matched your search
@@ -46,6 +46,7 @@ import {
|
||||
import { medicationExtractionSchema } from "@/lib/ai/coach/medication-extract-prompt";
|
||||
import { ACCEPTED_INSIGHTS_TILE_IDS } from "@/lib/insights-layout";
|
||||
import { exportSelectionSchema } from "@/lib/validations/health-record-export";
|
||||
import { METRIC_STATUS_IDS } from "@/lib/insights/metric-status-registry";
|
||||
|
||||
/**
|
||||
* Common envelopes — every HealthLog API response wraps payload in
|
||||
@@ -1097,6 +1098,64 @@ const insightsComprehensiveResponse = z
|
||||
"AI-generated insights bundle. Strict-schema validated server-side; Coach-routed when the insight surface needs day-level grounding.",
|
||||
});
|
||||
|
||||
// v1.8.7.1 — generic per-HealthKit-metric assessment. The query enum is
|
||||
// derived from the same registry the route validates against, so the
|
||||
// spec, the route, and the cache scope cannot drift. The seven
|
||||
// specialised metrics (weight / blood-pressure / pulse / bmi / mood /
|
||||
// medication-compliance) keep their own routes and are NOT accepted here.
|
||||
const metricStatusQuery = z
|
||||
.object({
|
||||
metric: z
|
||||
.enum(METRIC_STATUS_IDS as [string, ...string[]])
|
||||
.describe(
|
||||
"HealthKit metric id to assess (e.g. RESTING_HEART_RATE, SLEEP_DURATION). Closed enum: an unknown id 422s. The seven specialised metrics are served by their own routes and are not accepted here.",
|
||||
),
|
||||
locale: z
|
||||
.enum(["de", "en"])
|
||||
.optional()
|
||||
.describe("Optional UI-locale override; defaults to the session locale."),
|
||||
})
|
||||
.meta({ id: "MetricStatusQuery" });
|
||||
|
||||
const metricStatusResponse = z
|
||||
.object({
|
||||
hasProvider: z
|
||||
.boolean()
|
||||
.describe(
|
||||
"False when the user has no usable AI provider — `text` then carries the generic no-key guidance.",
|
||||
),
|
||||
text: z
|
||||
.string()
|
||||
.nullable()
|
||||
.describe(
|
||||
"The assessment narrative (plain text, rendered as React text children). Null while a first generation is preparing, or when the metric has insufficient data.",
|
||||
),
|
||||
cached: z
|
||||
.boolean()
|
||||
.describe("True when `text` is served from cache (incl. last-good)."),
|
||||
updatedAt: z.iso
|
||||
.datetime({ offset: true })
|
||||
.nullable()
|
||||
.describe("When the served assessment was generated; null when none."),
|
||||
preparing: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(
|
||||
"True when a first assessment is being generated out of band and no prior text exists yet — the client polls until it lands.",
|
||||
),
|
||||
insufficient: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(
|
||||
"True when the metric has no readings; no assessment is generated (no LLM call). The card shows its insufficient-data state.",
|
||||
),
|
||||
})
|
||||
.meta({
|
||||
id: "MetricStatusResponse",
|
||||
description:
|
||||
"Generic per-metric assessment envelope. Identical shape to the seven specialised `*-status` cards so the `InsightStatusCard` consumes it unchanged. Read-only + stale-while-revalidate: a cache miss warms a generation out of band and serves the last-good text meanwhile.",
|
||||
});
|
||||
|
||||
// v1.7.0 — unified dashboard first-paint snapshot. One GET that
|
||||
// assembles every above-the-fold tile field in a single round-trip.
|
||||
// Two-phase shape: `tiles` (fast, always present) + `extras` (thick,
|
||||
@@ -2350,6 +2409,31 @@ export const openApiPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/insights/metric-status": {
|
||||
get: {
|
||||
tags: ["Insights"],
|
||||
summary: "Generic per-HealthKit-metric assessment",
|
||||
description:
|
||||
"v1.8.7.1 — data-driven plain-language assessment for any registered HealthKit metric (resting heart rate, sleep, glucose, body composition, gait, audio exposure, …). One generic route covering ~30 metric pages via archetype prompt templates + per-metric metadata. Read-only: a cache miss warms a generation out of band and serves the last-good text meanwhile (stale-while-revalidate). An unknown `metric` 422s against the closed registry enum. Auth via cookie or Bearer.",
|
||||
requestParams: {
|
||||
query: metricStatusQuery,
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Assessment envelope (fresh, cached, or preparing).",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
metricStatusResponse,
|
||||
"MetricStatusResponseEnvelope",
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
...stdResponses,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const openApiComponents: NonNullable<ZodOpenApiObject["components"]> = {
|
||||
|
||||
Reference in new issue
Block a user