mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
feat(ai): answer the status notes with a null note when AI is unavailable
The status family (the five metric cards, medication compliance, the generic metric status and the biomarker assessment) asked the retired switch and the `insights` module and refused with 403. The note is model text and the card around it is data, so they are mixed reads now: while the `statusText` capability is available the note is served and warmed as before, with `ai` beside it; otherwise the answer is 200 with the note null, `preparing: false`, `hasProvider` as provider presence only and `ai` naming the reason, and neither the cache nor the warm queue is touched. The generic metric status still reports `insufficient` from the data. The mood card keeps its mood module gate. Kept apart so it can be held back while the shipped iPhone app's rendering of a 200 without a note is confirmed. Refs #975
This commit is contained in:
1 parent
9f1d1341f9
commit
1f6a13e2f7
19 files changed
+621
-207
No files matched your search
@@ -596,7 +596,7 @@ export const insightsPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
tags: ["Insights"],
|
||||
summary: "Blood-pressure assessment",
|
||||
description:
|
||||
"Data-driven plain-language assessment of the user's recent blood-pressure readings. Read-only: a cache miss warms a generation out of band and serves the last-good text meanwhile (stale-while-revalidate). Auth via cookie or Bearer.",
|
||||
"Data-driven plain-language assessment of the user's recent blood-pressure readings. Read-only: a cache miss warms a generation out of band and serves the last-good text meanwhile (stale-while-revalidate). The note is model text: it is served, and warmed, only while the `statusText` AI capability is available. Otherwise the answer is 200 with the note null, `preparing` false, `hasProvider` as provider presence and `ai` naming the reason; nothing is read from the cache or queued. Never refused for an AI reason, and the `insights` module (the AI analysis opt-out) is folded into the capability. Auth via cookie or Bearer.",
|
||||
requestParams: {
|
||||
query: insightStatusQuery,
|
||||
},
|
||||
@@ -622,7 +622,7 @@ export const insightsPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
tags: ["Insights"],
|
||||
summary: "Pulse assessment",
|
||||
description:
|
||||
"Data-driven plain-language assessment of the user's recent resting-pulse readings. Read-only: a cache miss warms a generation out of band and serves the last-good text meanwhile (stale-while-revalidate). Auth via cookie or Bearer.",
|
||||
"Data-driven plain-language assessment of the user's recent resting-pulse readings. Read-only: a cache miss warms a generation out of band and serves the last-good text meanwhile (stale-while-revalidate). The note is model text: it is served, and warmed, only while the `statusText` AI capability is available. Otherwise the answer is 200 with the note null, `preparing` false, `hasProvider` as provider presence and `ai` naming the reason; nothing is read from the cache or queued. Never refused for an AI reason, and the `insights` module (the AI analysis opt-out) is folded into the capability. Auth via cookie or Bearer.",
|
||||
requestParams: {
|
||||
query: insightStatusQuery,
|
||||
},
|
||||
@@ -648,7 +648,7 @@ export const insightsPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
tags: ["Insights"],
|
||||
summary: "Weight assessment",
|
||||
description:
|
||||
"Data-driven plain-language assessment of the user's recent weight trend. Read-only: a cache miss warms a generation out of band and serves the last-good text meanwhile (stale-while-revalidate). Auth via cookie or Bearer.",
|
||||
"Data-driven plain-language assessment of the user's recent weight trend. Read-only: a cache miss warms a generation out of band and serves the last-good text meanwhile (stale-while-revalidate). The note is model text: it is served, and warmed, only while the `statusText` AI capability is available. Otherwise the answer is 200 with the note null, `preparing` false, `hasProvider` as provider presence and `ai` naming the reason; nothing is read from the cache or queued. Never refused for an AI reason, and the `insights` module (the AI analysis opt-out) is folded into the capability. Auth via cookie or Bearer.",
|
||||
requestParams: {
|
||||
query: insightStatusQuery,
|
||||
},
|
||||
@@ -674,7 +674,7 @@ export const insightsPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
tags: ["Insights"],
|
||||
summary: "BMI assessment",
|
||||
description:
|
||||
"Data-driven plain-language assessment of the user's body-mass index. Read-only: a cache miss warms a generation out of band and serves the last-good text meanwhile (stale-while-revalidate). Auth via cookie or Bearer.",
|
||||
"Data-driven plain-language assessment of the user's body-mass index. Read-only: a cache miss warms a generation out of band and serves the last-good text meanwhile (stale-while-revalidate). The note is model text: it is served, and warmed, only while the `statusText` AI capability is available. Otherwise the answer is 200 with the note null, `preparing` false, `hasProvider` as provider presence and `ai` naming the reason; nothing is read from the cache or queued. Never refused for an AI reason, and the `insights` module (the AI analysis opt-out) is folded into the capability. Auth via cookie or Bearer.",
|
||||
requestParams: {
|
||||
query: insightStatusQuery,
|
||||
},
|
||||
@@ -700,7 +700,7 @@ export const insightsPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
tags: ["Insights"],
|
||||
summary: "Mood assessment",
|
||||
description:
|
||||
"Data-driven plain-language assessment of the user's recent mood entries. Read-only: a cache miss warms a generation out of band and serves the last-good text meanwhile (stale-while-revalidate). Auth via cookie or Bearer.",
|
||||
"Data-driven plain-language assessment of the user's recent mood entries. Read-only: a cache miss warms a generation out of band and serves the last-good text meanwhile (stale-while-revalidate). The note is model text: it is served, and warmed, only while the `statusText` AI capability is available. Otherwise the answer is 200 with the note null, `preparing` false, `hasProvider` as provider presence and `ai` naming the reason; nothing is read from the cache or queued. Never refused for an AI reason, and the `insights` module (the AI analysis opt-out) is folded into the capability. Auth via cookie or Bearer.",
|
||||
requestParams: {
|
||||
query: insightStatusQuery,
|
||||
},
|
||||
@@ -726,7 +726,7 @@ export const insightsPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
tags: ["Insights"],
|
||||
summary: "Medication-compliance assessment",
|
||||
description:
|
||||
"Data-driven plain-language assessment of the user's medication compliance — an overall `summary` plus a per-medication note array. Read-only: a cache miss warms a generation out of band and serves the last-good envelope meanwhile (stale-while-revalidate). Auth via cookie or Bearer.",
|
||||
"Data-driven plain-language assessment of the user's medication compliance — an overall `summary` plus a per-medication note array. Read-only: a cache miss warms a generation out of band and serves the last-good envelope meanwhile (stale-while-revalidate). The note is model text: it is served, and warmed, only while the `statusText` AI capability is available. Otherwise the answer is 200 with the note null, `preparing` false, `hasProvider` as provider presence and `ai` naming the reason; nothing is read from the cache or queued. Never refused for an AI reason, and the `insights` module (the AI analysis opt-out) is folded into the capability. Auth via cookie or Bearer.",
|
||||
requestParams: {
|
||||
query: insightStatusQuery,
|
||||
},
|
||||
@@ -753,7 +753,7 @@ export const insightsPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
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.",
|
||||
"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. The note is model text: it is served, and warmed, only while the `statusText` AI capability is available. Otherwise the answer is 200 with the note null, `preparing` false, `hasProvider` as provider presence and `ai` naming the reason; nothing is read from the cache or queued. Never refused for an AI reason, and the `insights` module (the AI analysis opt-out) is folded into the capability. Auth via cookie or Bearer.",
|
||||
requestParams: {
|
||||
query: metricStatusQuery,
|
||||
},
|
||||
@@ -779,7 +779,7 @@ export const insightsPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
tags: ["Insights"],
|
||||
summary: "Per-biomarker assessment",
|
||||
description:
|
||||
"Data-driven plain-language assessment for one user-scoped lab biomarker, reading its `LabResult` history. Identical envelope to the metric-status card so the `InsightStatusCard` consumes it unchanged. Read-only: a cache miss warms a generation out of band and serves the last-good text meanwhile (stale-while-revalidate); the assessment regenerates only when a new reading lands. A marker with no numeric readings returns `insufficient` without an LLM call. Auth via cookie or Bearer.",
|
||||
"Data-driven plain-language assessment for one user-scoped lab biomarker, reading its `LabResult` history. Identical envelope to the metric-status card so the `InsightStatusCard` consumes it unchanged. Read-only: a cache miss warms a generation out of band and serves the last-good text meanwhile (stale-while-revalidate); the assessment regenerates only when a new reading lands. A marker with no numeric readings returns `insufficient` without an LLM call. The note is model text: it is served, and warmed, only while the `statusText` AI capability is available. Otherwise the answer is 200 with the note null, `preparing` false, `hasProvider` as provider presence and `ai` naming the reason; nothing is read from the cache or queued. Never refused for an AI reason, and the `insights` module (the AI analysis opt-out) is folded into the capability. Auth via cookie or Bearer.",
|
||||
requestParams: {
|
||||
query: biomarkerAssessmentQuery,
|
||||
},
|
||||
|
||||
@@ -88,13 +88,13 @@ export const metricStatusResponse = z
|
||||
hasProvider: z
|
||||
.boolean()
|
||||
.describe(
|
||||
"False when the user has no usable AI provider — `text` then carries the generic no-key guidance.",
|
||||
"Whether a configured AI provider exists for the record: provider presence and nothing else. It is never false because the operator switched notes off or consent is missing; `ai` says that.",
|
||||
),
|
||||
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.",
|
||||
"The assessment note (plain text, rendered as React text children). Null while a first generation is preparing, when the metric has insufficient data, and whenever the `statusText` capability is unavailable (see `ai`).",
|
||||
),
|
||||
cached: z
|
||||
.boolean()
|
||||
@@ -119,8 +119,11 @@ export const metricStatusResponse = 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.",
|
||||
"True when the metric has no readings; no assessment is generated (no LLM call). The card shows its insufficient-data state. Still computed while `ai` is unavailable.",
|
||||
),
|
||||
ai: aiCapabilityState.describe(
|
||||
"The `statusText` capability behind the note. While it is unavailable the note is null, `preparing` is false, the cache is not read and nothing is warmed.",
|
||||
),
|
||||
})
|
||||
.meta({
|
||||
id: "MetricStatusResponse",
|
||||
@@ -728,13 +731,13 @@ export const insightStatusResponse = z
|
||||
hasProvider: z
|
||||
.boolean()
|
||||
.describe(
|
||||
"False when the user has no usable AI provider — `text` then carries the generic no-key guidance.",
|
||||
"Whether a configured AI provider exists for the record: provider presence and nothing else. It is never false because the operator switched notes off or consent is missing; `ai` says that.",
|
||||
),
|
||||
text: z
|
||||
.string()
|
||||
.nullable()
|
||||
.describe(
|
||||
"The assessment narrative (plain text, rendered as React text children). Null while a first generation is preparing.",
|
||||
"The assessment note (plain text, rendered as React text children). Null while a first generation is preparing, and whenever the `statusText` capability is unavailable (see `ai`).",
|
||||
),
|
||||
cached: z
|
||||
.boolean()
|
||||
@@ -755,6 +758,9 @@ export const insightStatusResponse = z
|
||||
.describe(
|
||||
"True when `text` is served from last-good cache (stale-while-revalidate) while a fresh generation is in flight. The client keeps polling on `preparing || revalidating` (bounded) so the open card upgrades to the warmed assessment without a remount.",
|
||||
),
|
||||
ai: aiCapabilityState.describe(
|
||||
"The `statusText` capability behind the note. While it is unavailable the note is null, `preparing` is false, the cache is not read and nothing is warmed.",
|
||||
),
|
||||
})
|
||||
.meta({
|
||||
id: "InsightStatusResponse",
|
||||
@@ -786,13 +792,13 @@ export const biomarkerAssessmentResponse = z
|
||||
hasProvider: z
|
||||
.boolean()
|
||||
.describe(
|
||||
"False when the user has no usable AI provider — `text` then carries the generic no-key guidance.",
|
||||
"Whether a configured AI provider exists for the record: provider presence and nothing else. It is never false because the operator switched notes off or consent is missing; `ai` says that.",
|
||||
),
|
||||
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 marker has no numeric readings.",
|
||||
"The assessment note (plain text, rendered as React text children). Null while a first generation is preparing, when the marker has no numeric readings, and whenever the `statusText` capability is unavailable (see `ai`).",
|
||||
),
|
||||
cached: z
|
||||
.boolean()
|
||||
@@ -819,6 +825,9 @@ export const biomarkerAssessmentResponse = z
|
||||
.describe(
|
||||
"True when the marker has no numeric readings; no assessment is generated (no LLM call). The card is not rendered.",
|
||||
),
|
||||
ai: aiCapabilityState.describe(
|
||||
"The `statusText` capability behind the note. While it is unavailable the note is null, `preparing` is false, the cache is not read and nothing is warmed.",
|
||||
),
|
||||
})
|
||||
.meta({
|
||||
id: "BiomarkerAssessmentResponse",
|
||||
@@ -834,13 +843,13 @@ export const medicationComplianceStatusResponse = z
|
||||
hasProvider: z
|
||||
.boolean()
|
||||
.describe(
|
||||
"False when the user has no usable AI provider — `summary` then carries the generic no-key guidance.",
|
||||
"Whether a configured AI provider exists for the record: provider presence and nothing else. It is never false because the operator switched notes off or consent is missing; `ai` says that.",
|
||||
),
|
||||
summary: z
|
||||
.string()
|
||||
.nullable()
|
||||
.describe(
|
||||
"The overall compliance narrative (plain text). Null while a first generation is preparing.",
|
||||
"The overall compliance note (plain text). Null while a first generation is preparing, and whenever the `statusText` capability is unavailable (see `ai`).",
|
||||
),
|
||||
medications: z
|
||||
.array(
|
||||
@@ -879,6 +888,9 @@ export const medicationComplianceStatusResponse = z
|
||||
.describe(
|
||||
"True when the envelope is served from last-good cache (stale-while-revalidate) while a fresh generation is in flight. The client keeps polling on `preparing || revalidating` (bounded).",
|
||||
),
|
||||
ai: aiCapabilityState.describe(
|
||||
"The `statusText` capability behind the note. While it is unavailable `summary` is null, `medications` is empty, `preparing` is false, the cache is not read and nothing is warmed.",
|
||||
),
|
||||
})
|
||||
.meta({
|
||||
id: "MedicationComplianceStatusResponse",
|
||||
|
||||
Reference in new issue
Block a user